Repository: ucfopen/canvasapi (language: python)

Issue:
Enhancement: Create OutcomeResult class
Outcome results with `course.get_outcome_results()` are [returned as JSON](https://github.com/ucfopen/canvasapi/blob/3f1a71e23c86a49dbaa6e84f6c0e81c297b86e36/canvasapi/course.py#L1787-L1803). It's also not returned in a `PaginatedList`, so the call needs to either manually check for more results or the user has to call for larger batches. 

Returning in a PaginatedList and creating an OutcomeResult class will allow for easier manipulation of results.

```javascript
// A student's result for an outcome
{
  // A unique identifier for this result
  "id": 42,
  // The student's score
  "score": 6,
  // The datetime the resulting OutcomeResult was submitted at, or absent that,
  // when it was assessed.
  "submitted_or_assessed_at": "2013-02-01T00:00:00-06:00",
  // Unique identifiers of objects associated with this result
  "links": {"user":"3","learning_outcome":"97","alignment":"53"},
  // score's percent of maximum points possible for outcome, scaled to reflect any
  // custom mastery levels that differ from the learning outcome
  "percent": 0.65
}
```

An additional improvement would be to convert the `links` property to an `OutcomeLink` instance.


Discussion / hints:

Fix patch:
diff --git a/AUTHORS.md b/AUTHORS.md
index 8b36f70f..f82fa2ef 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -19,13 +19,16 @@ Patches and Suggestions
 - Anthony Rodriguez [@AnthonyRodriguez726](https://github.com/AnthonyRodriguez726)
 - Ashutosh Saxena [@Xx-Ashutosh-xX](https://github.com/Xx-Ashutosh-xX)
 - Ben Liblit [@liblit](https://github.com/liblit)
+- Benjamin Reed [@breed](https://github.com/breed)
 - Bill Wrbican [@wjw27](https://github.com/wjw27)
+- [@Birdmaaan4](https://github.com/Birdmaaan4)
 - [@blepabyte](https://github.com/blepabyte)
 - Bradford Lynch [@bradfordlynch](https://github.com/bradfordlynch)
 - Brian Bennett [@bennettscience](https://github.com/bennettscience)
 - Bruce Spang [@brucespang](https://github.com/brucespang)
 - Catherine Abbruzzese [@cat0698](https://github.com/cat0698))
 - Cameron Cuff [@ctcuff](https://github.com/ctcuff)
+- Craig Thompson [@craigdsthompson](https://github.com/craigdsthompson)
 - Dalton Durst [@UniversalSuperBox](https://github.com/UniversalSuperBox)
 - Damian Sweeney [@damianfs](https://github.com/damianfs)
 - Daniel Brinkman [@DanBrink91](https://github.com/DanBrink91)
@@ -39,6 +42,7 @@ Patches and Suggestions
 - Elli Howard [@qwertynerd97](https://github.com/qwertynerd97)
 - Erik Tews [@eriktews](https://github.com/eriktews)
 - Ethan Finlay [@atarisafari](https://github.com/atarisafari)
+- Gabriel Garfinkel [@ggarfink](https://github.com/ggarfink)
 - Gabriela Dijkhoffz [@gdijkhoffz](https://github.com/gdijkhoffz)
 - Harlan Colclough [@hcolclou](https://github.com/hcolclou)
 - Henry Acevedo [@Colombiangmr](https://github.com/Colombiangmr)
@@ -51,6 +55,7 @@ Patches and Suggestions
 - Jonathan Guilbe [@JonGuilbe](https://github.com/JonGuilbe)
 - Jose Silveti [@jrsilveti](https://github.com/jrsilveti)
 - Joshua Salzedo [@theunkn0wn1](https://github.com/theunkn0wn1)
+- [@kailukaitisBrendan](https://github.com/kailukaitisBrendan)
 - Keegan Berry [@keeeeeegan](https://github.com/keeeeeegan)
 - Kenny Perez [@kennygperez](https://github.com/kennygperez)
 - [@kensler](https://github.com/kensler)
@@ -68,10 +73,13 @@ Patches and Suggestions
 - Philip Carter [@phillyc](https://github.com/phillyc)
 - Ralph Baird [@rmanbaird](https://github.com/rmanbaird)
 - [@Screeeech](https://github.com/Screeeech)
+- Shane Rosenkrantz [@shaneros](https://github.com/shaneros)
 - Sigurður Baldursson [@sigurdurb](https://github.com/sigurdurb)
 - Spencer Rogers [@spencer1248](https://github.com/spencer1248)
 - Stephen Woosley [@stephenwoosley](https://github.com/stephenwoosley)
+- Steven Bell [@stevenbell](https://github.com/stevenbell)
 - Steven Williams [@onomou](https://github.com/onomou)
+- Tinson Lai [@laitingsheng](https://github.com/laitingsheng)
 - Toan Vu [@vutoan1245](https://github.com/vutoan1245)
 - [@Tobiaqs](https://github.com/Tobiaqs)
 - Tuan Pham [@tuanvpham](https://github.com/tuanvpham)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 744f7397..532f4cc9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,35 @@
 
 ## [Unreleased]
 
+## [3.0.0] - 2022-09-21
+
+### 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))
+- List observers of a User (Thanks, [@bennettscience](https://github.com/bennettscience))
+- ePortfolio endpoints (Thanks, [@Birdmaaan4](https://github.com/Birdmaaan4) and [@bennettscience](https://github.com/bennettscience))
+- Delete an Admin from an Account (Thanks, [@shaneros](https://github.com/shaneros))
+
+### General
+
+- Added support for Python 3.10
+- Smart DateTimes now support any ISO 8601 format, including time offsets. (Thanks, [@kailukaitisBrendan](https://github.com/kailukaitisBrendan))
+
+### Bugfixes
+
+- Fixed an issue where kwargs were not passed along to Canvas in `User.get_profile()`. (Thanks, [@breed](https://github.com/breed))
+
+### Breaking Changes
+
+- Dropped support for Python 3.6
+- Update `QuizSubmission.get_submission_events` to return a `PaginatedList`. (Thanks, [@stevenbell](https://github.com/stevenbell))
+- Update `Course.get_course_level_student_summary_data` to return a `PaginatedList` of `CourseStudentSummary` items instead of a dictionary. (Thanks, [@craigdsthompson](https://github.com/craigdsthompson))
+- Update `Course.get_outcome_results` to return a `PaginatedList` of `OutcomeResult` items instead of a dictionary. (Thanks, [@bennettscience](https://github.com/bennettscience))
+- Remove unnecessary `id` parameter from `delete`, `reorder_question_group`, and `update` methods in `QuizGroup` class. (Thanks, [@kailukaitisBrendan](https://github.com/kailukaitisBrendan))
+- Update `Submission` to return attachments as `File` objects instead of dictionaries. (Thanks, [@laitingsheng](https://github.com/laitingsheng))
+
 ## [2.2.0] - 2021-03-25
 
 ### New Endpoint Coverage
@@ -534,7 +563,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/v2.2.0...develop
+[Unreleased]: https://github.com/ucfopen/canvasapi/compare/v3.0.0...develop
+[3.0.0]: https://github.com/ucfopen/canvasapi/compare/v2.2.0...v3.0.0
 [2.2.0]: https://github.com/ucfopen/canvasapi/compare/v2.1.0...v2.2.0
 [2.1.0]: https://github.com/ucfopen/canvasapi/compare/v2.0.0...v2.1.0
 [2.0.0]: https://github.com/ucfopen/canvasapi/compare/v1.0.0...v2.0.0
diff --git a/README.md b/README.md
index 274560ff..df6d18e5 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,9 @@
 [![License](https://img.shields.io/pypi/l/canvasapi.svg)](https://pypi.python.org/pypi/canvasapi)
 [![Python Versions](https://img.shields.io/pypi/pyversions/canvasapi.svg)](https://pypi.python.org/pypi/canvasapi)
 [![Documentation Status](https://readthedocs.org/projects/canvasapi/badge/?version=stable)](http://canvasapi.readthedocs.io/en/stable/?badge=stable)
-[![Build Status](https://travis-ci.org/ucfopen/canvasapi.svg?branch=master)](https://travis-ci.org/ucfopen/canvasapi)
-[![Coverage Status](https://coveralls.io/repos/github/ucfopen/canvasapi/badge.svg?branch=master)](https://coveralls.io/github/ucfopen/canvasapi?branch=master)
-[![Join UCF Open Slack Discussions](https://ucf-open-slackin.herokuapp.com/badge.svg)](https://ucf-open-slackin.herokuapp.com/)
+[![Build Status](https://github.com/ucfopen/canvasapi/actions/workflows/run-tests.yml/badge.svg?branch=develop)](https://github.com/ucfopen/canvasapi/actions)
+[![codecov](https://codecov.io/gh/ucfopen/canvasapi/branch/develop/graph/badge.svg?token=CFNpp8f56M)](https://codecov.io/gh/ucfopen/canvasapi)
+[![Join UCF Open Slack Discussions](https://badgen.net/badge/icon/ucfopen?icon=slack&label=slack&color=pink)](https://dl.ucf.edu/join-ucfopen)
 [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
 
 # CanvasAPI
@@ -167,4 +167,4 @@ If you have a project that uses CanvasAPI that you'd like to promote, please con
 
 ## Contact Us
 
-Need help? Have an idea? Feel free to check out our [Discussions](https://github.com/ucfopen/canvasapi/discussions) board. Just want to say hi or get extended spport? Come join us on the [UCF Open Slack Channel](https://ucf-open-slackin.herokuapp.com) and join the `#canvasapi` channel!
+Need help? Have an idea? Feel free to check out our [Discussions](https://github.com/ucfopen/canvasapi/discussions) board. Just want to say hi or get extended spport? Come join us on the [UCF Open Slack Channel](https://dl.ucf.edu/join-ucfopen) and join the `#canvasapi` channel!
diff --git a/canvasapi/__init__.py b/canvasapi/__init__.py
index 40a49573..45b18f83 100644
--- a/canvasapi/__init__.py
+++ b/canvasapi/__init__.py
@@ -4,4 +4,4 @@
 
 __all__ = ["Canvas"]
 
-__version__ = "2.2.0"
+__version__ = "3.0.0"
diff --git a/canvasapi/account.py b/canvasapi/account.py
index 3877d4e5..2aa37ee5 100644
--- a/canvasapi/account.py
+++ b/canvasapi/account.py
@@ -545,6 +545,31 @@ def delete(self, **kwargs):
 
         return response.json().get("workflow_state") == "deleted"
 
+    def delete_admin(self, user, **kwargs):
+        """
+        Remove an admin role from an existing user in the current account.
+
+        :calls: `DELETE /api/v1/accounts/:account_id/admins/:user_id \
+        <https://canvas.instructure.com/doc/api/admins.html#method.admins.destroy>`_
+
+        :param user: The user object or ID to remove as admin.
+        :type user: :class:`canvasapi.user.User` or int
+
+        :rtype: :class:`canvasapi.account.Admin`
+        """
+        from canvasapi.user import User
+
+        user_id = obj_or_id(user, "user", (User,))
+        kwargs["user_id"] = user_id
+
+        response = self._requester.request(
+            "DELETE",
+            "accounts/{}/admins/{}".format(self.id, user_id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        return Admin(self._requester, response.json())
+
     def delete_grading_period(self, grading_period, **kwargs):
         """
         Delete a grading period for an account.
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/canvas.py b/canvasapi/canvas.py
index 51b632ca..ede61940 100644
--- a/canvasapi/canvas.py
+++ b/canvasapi/canvas.py
@@ -5,6 +5,7 @@
 from canvasapi.course import Course
 from canvasapi.course_epub_export import CourseEpubExport
 from canvasapi.current_user import CurrentUser
+from canvasapi.eportfolio import EPortfolio
 from canvasapi.exceptions import RequiredFieldMissing
 from canvasapi.file import File
 from canvasapi.folder import Folder
@@ -771,6 +772,27 @@ def get_current_user(self):
         """
         return CurrentUser(self.__requester)
 
+    def get_eportfolio(self, eportfolio, **kwargs):
+        """
+        Get an eportfolio by ID.
+
+        :param eportfolio: The object or ID of the eportfolio to retrieve.
+        :type eportfolio: :class: `canvasapi.eportfolio.EPortfolio` or int
+
+        :calls: `GET /api/v1/eportfolios/:id` \
+            `<https://canvas.instructure.com/doc/api/e_portfolios.html#method.eportfolios_api.show>`_
+
+        :rtype: :class:`canvasapi.eportfolio.EPortfolio`
+        """
+        eportfolio_id = obj_or_id(eportfolio, "eportfolio", (EPortfolio,))
+        response = self.__requester.request(
+            "GET",
+            "eportfolios/{}".format(eportfolio_id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        return EPortfolio(self.__requester, response.json())
+
     def get_epub_exports(self, **kwargs):
         """
         Return a list of epub exports for the associated course.
diff --git a/canvasapi/canvas_object.py b/canvasapi/canvas_object.py
index 5a743eba..65d7679f 100644
--- a/canvasapi/canvas_object.py
+++ b/canvasapi/canvas_object.py
@@ -1,10 +1,6 @@
-import re
-from datetime import datetime
-
+import arrow
 import pytz
 
-DATE_PATTERN = re.compile("[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z")
-
 
 class CanvasObject(object):
     """
@@ -65,8 +61,11 @@ def set_attributes(self, attributes):
         for attribute, value in attributes.items():
             self.__setattr__(attribute, value)
 
-            # datetime field
-            if DATE_PATTERN.match(str(value)):
-                naive = datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ")
-                aware = naive.replace(tzinfo=pytz.utc)
+            try:
+                naive = arrow.get(str(value)).datetime
+                aware = naive.replace(tzinfo=pytz.utc) - naive.utcoffset()
                 self.__setattr__(attribute + "_date", aware)
+            except arrow.ParserError:
+                pass
+            except ValueError:
+                pass
diff --git a/canvasapi/content_migration.py b/canvasapi/content_migration.py
index fa5a7928..0679bdd0 100644
--- a/canvasapi/content_migration.py
+++ b/canvasapi/content_migration.py
@@ -186,6 +186,48 @@ def get_progress(self, **kwargs):
         )
         return Progress(self._requester, response.json())
 
+    def get_selective_data(self, **kwargs):
+        """
+        Return the selective data associated with this content migration. Use this to get a list
+        of available objects to import for a migration created with 'waiting_for_select=True'.
+
+        :calls:
+            `GET
+            /api/v1/accounts/:account_id/content_migrations/:content_migration_id/selective_data
+            <https://canvas.instructure.com/doc/api/content_migrations.html#method.content_migrations.content_list>`_
+
+            or `GET
+            /api/v1/courses/:course_id/content_migrations/:content_migration_id/selective_data
+            <https://canvas.instructure.com/doc/api/content_migrations.html#method.content_migrations.content_list>`_
+
+            or `GET
+            /api/v1/groups/:group_id/content_migrations/:content_migration_id/selective_data
+            <https://canvas.instructure.com/doc/api/content_migrations.html#method.content_migrations.content_list>`_
+
+            or `GET
+            /api/v1/users/:user_id/content_migrations/:content_migration_id/selective_data
+            <https://canvas.instructure.com/doc/api/content_migrations.html#method.content_migrations.content_list>`_
+
+        :returns: Paginated List of of nodes available for selection in this migration
+        :rtype: :class:`canvasapi.content_migration.ContentMigrationSelectionNode`
+        """
+        from canvasapi.content_migration import ContentMigrationSelectionNode
+
+        return PaginatedList(
+            ContentMigrationSelectionNode,
+            self._requester,
+            "GET",
+            "{}s/{}/content_migrations/{}/selective_data".format(
+                self._parent_type, self._parent_id, self.id
+            ),
+            {
+                "context_type": self._parent_type,
+                "context_id": self._parent_id,
+                "content_migration_id": self.id,
+            },
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
     def update(self, **kwargs):
         """
         Update an existing content migration.
@@ -220,6 +262,11 @@ def update(self, **kwargs):
             return False
 
 
+class ContentMigrationSelectionNode(CanvasObject):
+    def __str__(self):
+        return "{}".format(self.type)
+
+
 class MigrationIssue(CanvasObject):
     def __str__(self):
         return "{}: {}".format(self.id, self.description)
diff --git a/canvasapi/course.py b/canvasapi/course.py
index fcb97f05..04351e0c 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,
@@ -528,6 +529,7 @@ def create_rubric(self, **kwargs):
 
         if "rubric" in dictionary:
             r_dict = dictionary["rubric"]
+            r_dict.update({"course_id": self.id})
             rubric = Rubric(self._requester, r_dict)
 
             rubric_dict = {"rubric": rubric}
@@ -1050,17 +1052,19 @@ def get_course_level_student_summary_data(self, **kwargs):
         :calls: `GET /api/v1/courses/:course_id/analytics/student_summaries \
         <https://canvas.instructure.com/doc/api/analytics.html#method.analytics_api.course_student_summaries>`_
 
-        :rtype: dict
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+            :class:`canvasapi.course.CourseStudentSummary`
         """
 
-        response = self._requester.request(
+        return PaginatedList(
+            CourseStudentSummary,
+            self._requester,
             "GET",
             "courses/{}/analytics/student_summaries".format(self.id),
+            {"course_id": self.id},
             _kwargs=combine_kwargs(**kwargs),
         )
 
-        return response.json()
-
     def get_custom_columns(self, **kwargs):
         """
         List of all the custom gradebook columns for a course.
@@ -1411,6 +1415,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
@@ -1791,17 +1815,20 @@ def get_outcome_results(self, **kwargs):
         :calls: `GET /api/v1/courses/:course_id/outcome_results \
         <https://canvas.instructure.com/doc/api/outcome_results.html#method.outcome_results.index>`_
 
-        :returns: List of potential related outcome result dicts.
-        :rtype: dict
+        :returns: :class:`canvasapi.paginated_list.PaginatedList`
+            of :class:`canvasapi.outcome.OutcomeResult`
         """
-        response = self._requester.request(
+        from canvasapi.outcome import OutcomeResult
+
+        return PaginatedList(
+            OutcomeResult,
+            self._requester,
             "GET",
             "courses/{}/outcome_results".format(self.id),
+            _root="outcome_results",
             _kwargs=combine_kwargs(**kwargs),
         )
 
-        return response.json()
-
     def get_page(self, url, **kwargs):
         """
         Retrieve the contents of a wiki page.
@@ -1971,7 +1998,10 @@ def get_rubric(self, rubric_id, **kwargs):
             _kwargs=combine_kwargs(**kwargs),
         )
 
-        return Rubric(self._requester, response.json())
+        response_json = response.json()
+        response_json.update({"course_id": self.id})
+
+        return Rubric(self._requester, response_json)
 
     def get_rubrics(self, **kwargs):
         """
@@ -1988,6 +2018,7 @@ def get_rubrics(self, **kwargs):
             self._requester,
             "GET",
             "courses/%s/rubrics" % (self.id),
+            {"course_id": self.id},
             _kwargs=combine_kwargs(**kwargs),
         )
 
@@ -2661,6 +2692,11 @@ def remove(self, **kwargs):
         return CourseNickname(self._requester, response.json())
 
 
+class CourseStudentSummary(CanvasObject):
+    def __str__(self):
+        return "Course Student Summary {}".format(self.id)
+
+
 class LatePolicy(CanvasObject):
     def __str__(self):
         return "Late Policy {}".format(self.id)
diff --git a/canvasapi/eportfolio.py b/canvasapi/eportfolio.py
new file mode 100644
index 00000000..f0eecb86
--- /dev/null
+++ b/canvasapi/eportfolio.py
@@ -0,0 +1,86 @@
+from canvasapi.canvas_object import CanvasObject
+from canvasapi.paginated_list import PaginatedList
+from canvasapi.util import combine_kwargs
+
+
+class EPortfolio(CanvasObject):
+    def __str__(self):
+        return "{}".format(self.name)
+
+    def delete(self, **kwargs):
+        """
+        Delete an ePortfolio.
+
+        :calls: `DELETE /api/v1/eportfolios/:id \
+            <https://canvas.instructure.com/doc/api/e_portfolios.html#method.eportfolios_api.delete>`_
+
+        :returns: ePortfolio with deleted date set.
+        :rtype: :class:`canvasapi.eportfolio.EPortfolio`
+        """
+        response = self._requester.request(
+            "DELETE", "eportfolios/{}".format(self.id), _kwargs=combine_kwargs(**kwargs)
+        )
+        return EPortfolio(self._requester, response.json())
+
+    def get_eportfolio_pages(self, **kwargs):
+        """
+        Return a list of pages for an ePortfolio.
+
+        :calls: `GET /api/v1/eportfolios/:eportfolio_id/pages \
+            <https://canvas.instructure.com/doc/api/e_portfolios.html#method.eportfolios_api.pages>`_
+
+        :returns: List of ePortfolio pages.
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+            :class:`canvasapi.eportfolio.EPortfolioPage`
+        """
+
+        return PaginatedList(
+            EPortfolioPage,
+            self._requester,
+            "GET",
+            "eportfolios/{}/pages".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+    def moderate_eportfolio(self, **kwargs):
+        """
+        Update the spam_status of an eportfolio.
+        Only available to admins who can `moderate_user_content`.
+
+        :calls: `PUT /api/v1/eportfolios/:eportfolio_id/moderate \
+            <https://canvas.instructure.com/doc/api/e_portfolios.html#method.eportfolios_api.moderate>`_
+
+        :returns: Updated ePortfolio.
+        :rtype: :class:`canvasapi.eportfolio.EPortfolio`
+        """
+        response = self._requester.request(
+            "PUT",
+            "eportfolios/{}/moderate".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        return EPortfolio(self._requester, response.json())
+
+    def restore(self, **kwargs):
+        """
+        Restore an ePortfolio back to active that was previously deleted.
+        Only available to admins who can moderate_user_content.
+
+        :calls: `PUT /api/v1/eportfolios/:eportfolio_id/restore \
+            <https://canvas.instructure.com/doc/api/e_portfolios.html#method.eportfolios_api.restore>`_
+
+        :returns: Updated ePortfolio.
+        :rtype: :class:`canvasapi.eportfolio.EPortfolio`
+        """
+        response = self._requester.request(
+            "PUT",
+            "eportfolios/{}/restore".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        return EPortfolio(self._requester, response.json())
+
+
+class EPortfolioPage(CanvasObject):
+    def __str__(self):
+        return "{}. {}".format(self.position, self.name)
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/outcome.py b/canvasapi/outcome.py
index 1606a302..9adf2910 100644
--- a/canvasapi/outcome.py
+++ b/canvasapi/outcome.py
@@ -322,3 +322,8 @@ def import_outcome_group(self, outcome_group):
         )
 
         return OutcomeGroup(self._requester, response.json())
+
+
+class OutcomeResult(CanvasObject):
+    def __str__(self):
+        return "{} ({})".format(self.id, self.score)
diff --git a/canvasapi/quiz.py b/canvasapi/quiz.py
index 6b93253d..1602fb0f 100644
--- a/canvasapi/quiz.py
+++ b/canvasapi/quiz.py
@@ -561,19 +561,20 @@ def get_submission_events(self, **kwargs):
         :calls: `GET /api/v1/courses/:course_id/quizzes/:quiz_id/submissions/:id/events \
         <https://canvas.instructure.com/doc/api/quiz_submission_events.html#method.quizzes/quiz_submission_events_api.index>`_
 
-        :returns: list of QuizSubmissionEvents.
-        :rtype: list
+        :returns: PaginatedList of QuizSubmissionEvents.
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+          :class:`canvasapi.quiz.QuizSubmissionEvent`
         """
-        response = self._requester.request(
+        return PaginatedList(
+            QuizSubmissionEvent,
+            self._requester,
             "GET",
             "courses/{}/quizzes/{}/submissions/{}/events".format(
                 self.course_id, self.quiz_id, self.id
             ),
+            _root="quiz_submission_events",
             _kwargs=combine_kwargs(**kwargs),
         )
-        events = response.json()["quiz_submission_events"]
-
-        return [QuizSubmissionEvent(self._requester, event) for event in events]
 
     def get_submission_questions(self, **kwargs):
         """
diff --git a/canvasapi/quiz_group.py b/canvasapi/quiz_group.py
index 07924063..08917add 100644
--- a/canvasapi/quiz_group.py
+++ b/canvasapi/quiz_group.py
@@ -7,35 +7,32 @@ class QuizGroup(CanvasObject):
     def __str__(self):
         return "{} ({})".format(self.name, self.id)
 
-    def delete(self, id, **kwargs):
+    def delete(self, **kwargs):
         """
         Get details of the quiz group with the given id.
 
         :calls: `DELETE /api/v1/courses/:course_id/quizzes/:quiz_id/groups/:id \
         <https://canvas.instructure.com/doc/api/quiz_question_groups.html#method.quizzes/quiz_groups.destroy>`_
 
-        :param id: The ID of the question group.
-        :type id: int
-
         :returns: True if the result was successful (Status code of 204)
         :rtype: bool
         """
         response = self._requester.request(
             "DELETE",
-            "courses/{}/quizzes/{}/groups/{}".format(self.course_id, self.quiz_id, id),
+            "courses/{}/quizzes/{}/groups/{}".format(
+                self.course_id, self.quiz_id, self.id
+            ),
             _kwargs=combine_kwargs(**kwargs),
         )
         return response.status_code == 204
 
-    def reorder_question_group(self, id, order, **kwargs):
+    def reorder_question_group(self, order, **kwargs):
         """
         Update the order of questions within a given group
 
         :calls: `POST /api/v1/courses/:course_id/quizzes/:quiz_id/groups/:id/reorder \
         <https://canvas.instructure.com/doc/api/quiz_question_groups.html#method.quizzes/quiz_groups.reorder>`_
 
-        :param id: The ID of the question group.
-        :type id: int
         :param order: A list of dictionaries containing the key 'id' of
             the question to be placed at order's index.
         :type order: list[dict]
@@ -61,22 +58,20 @@ def reorder_question_group(self, id, order, **kwargs):
         response = self._requester.request(
             "POST",
             "courses/{}/quizzes/{}/groups/{}/reorder".format(
-                self.course_id, self.quiz_id, id
+                self.course_id, self.quiz_id, self.id
             ),
             _kwargs=combine_kwargs(**kwargs),
         )
 
         return response.status_code == 204
 
-    def update(self, id, quiz_groups, **kwargs):
+    def update(self, quiz_groups, **kwargs):
         """
         Update a question group given by id.
 
         :calls: `PUT /api/v1/courses/:course_id/quizzes/:quiz_id/groups/:id \
         <https://canvas.instructure.com/doc/api/quiz_question_groups.html#method.quizzes/quiz_groups.update>`_
 
-        :param id: The ID of the question group.
-        :type id: int
         :param quiz_groups: The name, pick count, and/or question points.
             All of these parameters are optional, but at least one must exist
             (even if empty) to recieve a response.
@@ -100,7 +95,9 @@ def update(self, id, quiz_groups, **kwargs):
 
         response = self._requester.request(
             "PUT",
-            "courses/{}/quizzes/{}/groups/{}".format(self.course_id, self.quiz_id, id),
+            "courses/{}/quizzes/{}/groups/{}".format(
+                self.course_id, self.quiz_id, self.id
+            ),
             _kwargs=combine_kwargs(**kwargs),
         )
 
diff --git a/canvasapi/rubric.py b/canvasapi/rubric.py
index a973e7e1..5ee003fe 100644
--- a/canvasapi/rubric.py
+++ b/canvasapi/rubric.py
@@ -6,6 +6,25 @@ class Rubric(CanvasObject):
     def __str__(self):
         return "{} ({})".format(self.title, self.id)
 
+    def delete(self, **kwargs):
+        """
+        Delete a Rubric.
+
+        :calls: `DELETE /api/v1/courses/:course_id/rubrics/:id \
+        <https://canvas.instructure.com/doc/api/rubrics.html#method.rubrics.destroy>`_
+
+        :rtype: :class:`canvasapi.rubric.Rubric`
+        """
+        from canvasapi.rubric import Rubric
+
+        response = self._requester.request(
+            "DELETE",
+            "courses/{}/rubrics/{}".format(self.course_id, self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        return Rubric(self._requester, response.json())
+
 
 class RubricAssociation(CanvasObject):
     def __str__(self):
diff --git a/canvasapi/submission.py b/canvasapi/submission.py
index 6e1250e0..f2d738c0 100644
--- a/canvasapi/submission.py
+++ b/canvasapi/submission.py
@@ -1,4 +1,5 @@
 from canvasapi.canvas_object import CanvasObject
+from canvasapi.file import File
 from canvasapi.paginated_list import PaginatedList
 from canvasapi.peer_review import PeerReview
 from canvasapi.upload import FileOrPathLike, Uploader
@@ -6,6 +7,14 @@
 
 
 class Submission(CanvasObject):
+    def __init__(self, requester, attributes):
+        super(Submission, self).__init__(requester, attributes)
+
+        self.attachments = [
+            File(requester, attachment)
+            for attachment in attributes.get("attachments", [])
+        ]
+
     def __str__(self):
         return "{}-{}".format(self.assignment_id, self.user_id)
 
diff --git a/canvasapi/user.py b/canvasapi/user.py
index 74c50791..3faafac4 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
@@ -24,7 +25,7 @@ def add_observee(self, observee_id, **kwargs):
 
         :param observee_id: The login id for the user to observe.
         :type observee_id: int
-        :rtype: :class: `canvasapi.user.User`
+        :rtype: :class:`canvasapi.user.User`
         """
 
         response = self._requester.request(
@@ -489,6 +490,25 @@ def get_enrollments(self, **kwargs):
             _kwargs=combine_kwargs(**kwargs),
         )
 
+    def get_eportfolios(self, **kwargs):
+        """
+        Returns a list of ePortfolios for a user.
+
+        :calls: `GET /api/v1/users/:user_id/eportfolios \
+            <https://canvas.instructure.com/doc/api/e_portfolios.html#method.eportfolios_api.index>`_
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+            :class:`canvasapi.eportfolio.EPortfolio`
+        """
+        from canvasapi.eportfolio import EPortfolio
+
+        return PaginatedList(
+            EPortfolio,
+            self._requester,
+            "GET",
+            "users/{}/eportfolios".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
     def get_feature_flag(self, feature, **kwargs):
         """
         Returns the feature flag that applies to the given user.
@@ -632,6 +652,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
@@ -712,6 +771,25 @@ def get_observees(self, **kwargs):
             _kwargs=combine_kwargs(**kwargs),
         )
 
+    def get_observers(self, **kwargs):
+        """
+        List the users that are observing the given user.
+
+        :calls:  `GET /api/v1/users/:user_id/observers \
+        <https://canvas.instructure.com/doc/api/user_observees.html#method.user_observees.observers>`_
+
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+            :class:`canvasapi.user.User`
+        """
+
+        return PaginatedList(
+            User,
+            self._requester,
+            "GET",
+            "users/{}/observers".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
     def get_open_poll_sessions(self, **kwargs):
         """
         Returns a paginated list of all opened poll sessions available to the current user.
@@ -762,7 +840,9 @@ def get_profile(self, **kwargs):
 
         :rtype: dict
         """
-        response = self._requester.request("GET", "users/{}/profile".format(self.id))
+        response = self._requester.request(
+            "GET", "users/{}/profile".format(self.id), _kwargs=combine_kwargs(**kwargs)
+        )
         return response.json()
 
     def get_user_logins(self, **kwargs):
@@ -807,6 +887,31 @@ def merge_into(self, destination_user, **kwargs):
         super(User, self).set_attributes(response.json())
         return self
 
+    def moderate_all_eportfolios(self, **kwargs):
+        """
+        Update the spam_status for all active eportfolios of a user.
+        Only available to admins who can moderate_user_content.
+
+        :param eportfolio: The object or ID of the ePortfolio to retrieve.
+        :type eportfolio: :class:`canvasapi.eportfolio.EPortfolio` or int
+
+        :calls: `PUT /api/v1/users/:user_id/eportfolios \
+            <https://canvas.instructure.com/doc/api/e_portfolios.html#method.eportfolios_api.moderate_all>`_
+
+        :returns: A list of all user ePortfolios.
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+            :class:`canvasapi.eportfolio.EPortfolio`
+        """
+        from canvasapi.eportfolio import EPortfolio
+
+        return PaginatedList(
+            EPortfolio,
+            self._requester,
+            "PUT",
+            "users/{}/eportfolios".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
     def remove_observee(self, observee_id, **kwargs):
         """
         Unregisters a user as being observed by the given user.
@@ -816,7 +921,7 @@ def remove_observee(self, observee_id, **kwargs):
 
         :param observee_id: The login id for the user to observe.
         :type observee_id: int
-        :rtype: :class: `canvasapi.user.User`
+        :rtype: :class:`canvasapi.user.User`
         """
 
         response = self._requester.request(
@@ -904,7 +1009,7 @@ def show_observee(self, observee_id, **kwargs):
 
         :param observee_id: The login id for the user to observe.
         :type observee_id: int
-        :rtype: :class: `canvasapi.user.User`
+        :rtype: :class:`canvasapi.user.User`
         """
 
         response = self._requester.request(
diff --git a/dev_requirements.txt b/dev_requirements.txt
index e89a2639..cfc2056e 100644
--- a/dev_requirements.txt
+++ b/dev_requirements.txt
@@ -1,7 +1,7 @@
 -r tests_requirements.txt
 
 docutils==0.15.2
-pre-commit; python_version >= '3.6'
+pre-commit
 Sphinx
 sphinx-rtd-theme
-sphinx-version-warning
\ No newline at end of file
+sphinx-version-warning
diff --git a/docs/account-ref.rst b/docs/account-ref.rst
index 176473de..a0ed5b54 100644
--- a/docs/account-ref.rst
+++ b/docs/account-ref.rst
@@ -19,6 +19,13 @@ AccountReport
 .. autoclass:: canvasapi.account.AccountReport
     :members:
 
+=====
+Admin
+=====
+
+.. autoclass:: canvasapi.account.Admin
+    :members:
+
 ====
 Role
 ====
diff --git a/docs/class-reference.rst b/docs/class-reference.rst
index cb5e00dd..21c0f2c7 100644
--- a/docs/class-reference.rst
+++ b/docs/class-reference.rst
@@ -27,12 +27,14 @@ Class Reference
     discussion-topic-ref
     enrollment-ref
     enrollment-term-ref
+    eportfolio-ref
     external-tool-ref
     favorite-ref
     feature-ref
     file-ref
     folder-ref
     grading-period-ref
+    grade-change-log-ref
     group-ref
     login-ref
     license-ref
diff --git a/docs/content-migration-ref.rst b/docs/content-migration-ref.rst
index 12c47c0f..95718619 100644
--- a/docs/content-migration-ref.rst
+++ b/docs/content-migration-ref.rst
@@ -5,6 +5,13 @@ ContentMigration
 .. autoclass:: canvasapi.content_migration.ContentMigration
     :members:
 
+=============================
+ContentMigrationSelectionNode
+=============================
+
+.. autoclass:: canvasapi.content_migration.ContentMigrationSelectionNode
+    :members:
+
 ==============
 MigrationIssue
 ==============
diff --git a/docs/course-ref.rst b/docs/course-ref.rst
index 3d43f672..02e77cf3 100644
--- a/docs/course-ref.rst
+++ b/docs/course-ref.rst
@@ -12,6 +12,13 @@ CourseNickname
 .. autoclass:: canvasapi.course.CourseNickname
     :members:
 
+====================
+CourseStudentSummary
+====================
+
+.. autoclass:: canvasapi.course.CourseStudentSummary
+    :members:
+
 ==========
 LatePolicy
 ==========
diff --git a/docs/eportfolio-ref.rst b/docs/eportfolio-ref.rst
new file mode 100644
index 00000000..922d5769
--- /dev/null
+++ b/docs/eportfolio-ref.rst
@@ -0,0 +1,13 @@
+==========
+EPortfolio
+==========
+
+.. autoclass:: canvasapi.eportfolio.EPortfolio
+    :members:
+
+==============
+EPortfolioPage
+==============
+
+.. autoclass:: canvasapi.eportfolio.EPortfolioPage
+    :members:
diff --git a/docs/getting-started.rst b/docs/getting-started.rst
index af403bb2..78514ab1 100644
--- a/docs/getting-started.rst
+++ b/docs/getting-started.rst
@@ -150,5 +150,3 @@ If Canvas returns an ISO 8601 formatted datetime string, CanvasAPI will automati
     '2014-02-11T16:38:00Z'
     >>> course.start_at_date
     datetime.datetime(2014, 2, 11, 16, 38, tzinfo=<UTC>)
-
-**Note**: At this time, the automatic conversion to `datetime` objects only works on strings with no UTC offset (ending in "Z").
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:
diff --git a/docs/outcome-ref.rst b/docs/outcome-ref.rst
index 079b17b6..7efff33d 100644
--- a/docs/outcome-ref.rst
+++ b/docs/outcome-ref.rst
@@ -18,3 +18,10 @@ OutcomeGroup
 
 .. autoclass:: canvasapi.outcome.OutcomeGroup
     :members:
+
+=============
+OutcomeResult
+=============
+
+.. autoclass:: canvasapi.outcome.OutcomeResult
+    :members:
diff --git a/requirements.txt b/requirements.txt
index 03c5fdd6..e8c9e31d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,3 @@
+arrow
 pytz
 requests
diff --git a/setup.py b/setup.py
index 212542bc..654f0a7a 100644
--- a/setup.py
+++ b/setup.py
@@ -29,7 +29,7 @@
     license="MIT License",
     packages=["canvasapi"],
     include_package_data=True,
-    install_requires=["pytz", "requests"],
+    install_requires=["arrow", "pytz", "requests"],
     zip_safe=False,
     classifiers=[
         "Development Status :: 5 - Production/Stable",
@@ -39,10 +39,10 @@
         "License :: OSI Approved :: MIT License",
         "Operating System :: OS Independent",
         "Programming Language :: Python",
-        "Programming Language :: Python :: 3.6",
         "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
         "Topic :: Software Development :: Libraries",
     ],
 )


Test patch:
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index acbd48fb..76c1c575 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -11,7 +11,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [3.6, 3.7, 3.8, 3.9]
+        python-version: ['3.7', '3.8', '3.9', '3.10']
 
     steps:
       - uses: actions/checkout@v2
diff --git a/tests/fixtures/account.json b/tests/fixtures/account.json
index 819ac408..a9feaeff 100644
--- a/tests/fixtures/account.json
+++ b/tests/fixtures/account.json
@@ -1687,6 +1687,26 @@
 			"workflow_state": "active"
 		}
 	},
+	"delete_admin": {
+		"method": "DELETE",
+		"endpoint": "accounts/1/admins/123",
+		"data": {
+			"id": 100,
+			"role": "AccountAdmin",
+			"role_id": 1,
+			"user": {
+				"id": 123,
+				"name": "John Doe",
+				"sortable_name": "John Doe",
+				"short_name": "J Doe",
+				"sis_user_id": null,
+				"integration_id": null,
+				"sis_import_id": null,
+				"login_id": "jdoe"
+			},
+			"workflow_state": "deleted"
+		}
+	},
 	"get_features": {
 		"method": "GET",
 		"endpoint": "accounts/1/features",
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/content_migration.json b/tests/fixtures/content_migration.json
index cb3b531a..2dab15f6 100644
--- a/tests/fixtures/content_migration.json
+++ b/tests/fixtures/content_migration.json
@@ -141,6 +141,368 @@
 		},
 		"status_code": 200
 	},
+	"get_selective_data_account": {
+		"method": "GET",
+		"endpoint": "accounts/1/content_migrations/1/selective_data",
+		"data": [
+			{
+			"content_migration_id": 1,
+			"context_id": 1,
+			"context_type": "account",
+			"property": "copy[all_account_settings]",
+			"title": "Account Settings",
+			"type": "account_settings"
+			},
+			{
+			"content_migration_id": 1,
+			"context_id": 1,
+			"context_type": "account",
+			"count": 2,
+			"property": "copy[all_assignments]",
+			"sub_items_url": "https://example.com/api/v1/accounts/1/content_migrations/1/selective_data?type=assignments",
+			"title": "Assignments",
+			"type": "assignments"
+			}
+		],
+		"status_code": 200
+	},
+	"get_selective_data_course": {
+		"method": "GET",
+		"endpoint": "courses/1/content_migrations/1/selective_data",
+		"data": [
+			{
+			"content_migration_id": 1,
+			"context_id": 1,
+			"context_type": "course",
+			"property": "copy[all_course_settings]",
+			"title": "Course Settings",
+			"type": "course_settings"
+			},
+			{
+			"content_migration_id": 1,
+			"context_id": 1,
+			"context_type": "course",
+			"count": 2,
+			"property": "copy[all_assignments]",
+			"sub_items_url": "https://example.com/api/v1/courses/1/content_migrations/1/selective_data?type=assignments",
+			"title": "Assignments",
+			"type": "assignments"
+			}
+		],
+		"status_code": 200
+	},
+	"get_selective_data_group": {
+		"method": "GET",
+		"endpoint": "groups/1/content_migrations/1/selective_data",
+		"data": [
+			{
+			"content_migration_id": 1,
+			"context_id": 1,
+			"context_type": "group",
+			"property": "copy[all_group_settings]",
+			"title": "Group Settings",
+			"type": "group_settings"
+			},
+			{
+			"content_migration_id": 1,
+			"context_id": 1,
+			"context_type": "group",
+			"count": 2,
+			"property": "copy[all_assignments]",
+			"sub_items_url": "https://example.com/api/v1/groups/1/content_migrations/1/selective_data?type=assignments",
+			"title": "Assignments",
+			"type": "assignments"
+			}
+		],
+		"status_code": 200
+	},
+	"get_selective_data_user": {
+		"method": "GET",
+		"endpoint": "users/1/content_migrations/1/selective_data",
+		"data": [
+			{
+			"content_migration_id": 1,
+			"context_id": 1,
+			"context_type": "user",
+			"property": "copy[all_user_settings]",
+			"title": "user Settings",
+			"type": "user_settings"
+			},
+			{
+			"content_migration_id": 1,
+			"context_id": 1,
+			"context_type": "user",
+			"count": 2,
+			"property": "copy[all_assignments]",
+			"sub_items_url": "https://example.com/api/v1/users/1/content_migrations/1/selective_data?type=assignments",
+			"title": "Assignments",
+			"type": "assignments"
+			}
+		],
+		"status_code": 200
+	},
+	"get_selective_data_assignments_account": {
+		"method": "GET",
+		"endpoint": "accounts/1/content_migrations/1/selective_data?type=assignments",
+		"data": [
+			{
+				"content_migration_id": 1,
+				"context_id": 1,
+				"context_type": "account",
+				"migration_id": "g8b133a71f8693ea12f549743bf59d194",
+				"property": "copy[assignment_groups][id_ga1f85a92f38ea154ea78bdf34af5a945]",
+				"sub_items": [
+				{
+					"linked_resource": {
+					"message": "linked with Discussion \"Topic Week 1: Discussion\"",
+					"migration_id": "ge1a27af717d06a0782d708c9441be1f68",
+					"property": "copy[discussion_topics][id_g1e4eabe30567ae83bfa62d08b88e56f08]",
+					"type": "discussion_topics"
+					},
+					"migration_id": "g486ff8a1e91a1d9403125c838ea2a75c8",
+					"property": "copy[assignments][id_g4c51f316971e47ac786d5cc1b16d2b198]",
+					"title": "Week 1: Discussion",
+					"type": "assignments"
+				},
+				{
+					"linked_resource": {
+					"message": "linked with Discussion Topic \"Week 2: Discussion\"",
+					"migration_id": "g047d0ed5637242f5623cb932e347ec3c5",
+					"property": "copy[discussion_topics][id_ge517692a894ba8a3e95a2a18655e13905]",
+					"type": "discussion_topics"
+					},
+					"migration_id": "g0d58cddab9615f2226a44c0aa44bf33eb",
+					"property": "copy[assignments][id_g2777dc21adf38a770697313c7ca693e8b]",
+					"title": "Week 2: Discussion",
+					"type": "assignments"
+				}
+				],
+				"title": "Discussion",
+				"type": "assignment_groups"
+			},
+			{
+				"content_migration_id": 1,
+				"context_id": 1,
+				"context_type": "account",
+				"migration_id": "gaba50c7f7eb473769f7dfb649f8ae6bac",
+				"property": "copy[assignment_groups][id_ge9c31bec83eeeff8c33e539293b824c5c]",
+				"sub_items": [
+				{
+					"migration_id": "g2d182762c23c79eec72fdbfaf4e72f497",
+					"property": "copy[assignments][id_g7510f7e9257131eedbf456fa851ce2727]",
+					"title": "Week 1: Assignment",
+					"type": "assignments"
+				},
+				{
+					"migration_id": "g0d4b9570357bf08230b2b5def78513da0",
+					"property": "copy[assignments][id_g8ed8a7082b0884b2ed9d1adcb2b0610e0]",
+					"title": "Week 2: Assignment",
+					"type": "assignments"
+				}
+				],
+				"title": "Assignments",
+				"type": "assignment_groups"
+			}
+			],
+		"status_code": 200
+	},
+
+	"get_selective_data_assignments_course": {
+		"method": "GET",
+		"endpoint": "courses/1/content_migrations/1/selective_data?type=assignments",
+		"data": [
+			{
+				"content_migration_id": 1,
+				"context_id": 1,
+				"context_type": "course",
+				"migration_id": "g8b133a71f8693ea12f549743bf59d194",
+				"property": "copy[assignment_groups][id_ga1f85a92f38ea154ea78bdf34af5a945]",
+				"sub_items": [
+				{
+					"linked_resource": {
+					"message": "linked with Discussion \"Topic Week 1: Discussion\"",
+					"migration_id": "ge1a27af717d06a0782d708c9441be1f68",
+					"property": "copy[discussion_topics][id_g1e4eabe30567ae83bfa62d08b88e56f08]",
+					"type": "discussion_topics"
+					},
+					"migration_id": "g486ff8a1e91a1d9403125c838ea2a75c8",
+					"property": "copy[assignments][id_g4c51f316971e47ac786d5cc1b16d2b198]",
+					"title": "Week 1: Discussion",
+					"type": "assignments"
+				},
+				{
+					"linked_resource": {
+					"message": "linked with Discussion Topic \"Week 2: Discussion\"",
+					"migration_id": "g047d0ed5637242f5623cb932e347ec3c5",
+					"property": "copy[discussion_topics][id_ge517692a894ba8a3e95a2a18655e13905]",
+					"type": "discussion_topics"
+					},
+					"migration_id": "g0d58cddab9615f2226a44c0aa44bf33eb",
+					"property": "copy[assignments][id_g2777dc21adf38a770697313c7ca693e8b]",
+					"title": "Week 2: Discussion",
+					"type": "assignments"
+				}
+				],
+				"title": "Discussion",
+				"type": "assignment_groups"
+			},
+			{
+				"content_migration_id": 1,
+				"context_id": 1,
+				"context_type": "course",
+				"migration_id": "gaba50c7f7eb473769f7dfb649f8ae6bac",
+				"property": "copy[assignment_groups][id_ge9c31bec83eeeff8c33e539293b824c5c]",
+				"sub_items": [
+				{
+					"migration_id": "g2d182762c23c79eec72fdbfaf4e72f497",
+					"property": "copy[assignments][id_g7510f7e9257131eedbf456fa851ce2727]",
+					"title": "Week 1: Assignment",
+					"type": "assignments"
+				},
+				{
+					"migration_id": "g0d4b9570357bf08230b2b5def78513da0",
+					"property": "copy[assignments][id_g8ed8a7082b0884b2ed9d1adcb2b0610e0]",
+					"title": "Week 2: Assignment",
+					"type": "assignments"
+				}
+				],
+				"title": "Assignments",
+				"type": "assignment_groups"
+			}
+			],
+		"status_code": 200
+	},
+	"get_selective_data_assignments_user": {
+		"method": "GET",
+		"endpoint": "users/1/content_migrations/1/selective_data?type=assignments",
+		"data": [
+			{
+				"content_migration_id": 1,
+				"context_id": 1,
+				"context_type": "user",
+				"migration_id": "g8b133a71f8693ea12f549743bf59d194",
+				"property": "copy[assignment_groups][id_ga1f85a92f38ea154ea78bdf34af5a945]",
+				"sub_items": [
+				{
+					"linked_resource": {
+					"message": "linked with Discussion \"Topic Week 1: Discussion\"",
+					"migration_id": "ge1a27af717d06a0782d708c9441be1f68",
+					"property": "copy[discussion_topics][id_g1e4eabe30567ae83bfa62d08b88e56f08]",
+					"type": "discussion_topics"
+					},
+					"migration_id": "g486ff8a1e91a1d9403125c838ea2a75c8",
+					"property": "copy[assignments][id_g4c51f316971e47ac786d5cc1b16d2b198]",
+					"title": "Week 1: Discussion",
+					"type": "assignments"
+				},
+				{
+					"linked_resource": {
+					"message": "linked with Discussion Topic \"Week 2: Discussion\"",
+					"migration_id": "g047d0ed5637242f5623cb932e347ec3c5",
+					"property": "copy[discussion_topics][id_ge517692a894ba8a3e95a2a18655e13905]",
+					"type": "discussion_topics"
+					},
+					"migration_id": "g0d58cddab9615f2226a44c0aa44bf33eb",
+					"property": "copy[assignments][id_g2777dc21adf38a770697313c7ca693e8b]",
+					"title": "Week 2: Discussion",
+					"type": "assignments"
+				}
+				],
+				"title": "Discussion",
+				"type": "assignment_groups"
+			},
+			{
+				"content_migration_id": 1,
+				"context_id": 1,
+				"context_type": "user",
+				"migration_id": "gaba50c7f7eb473769f7dfb649f8ae6bac",
+				"property": "copy[assignment_groups][id_ge9c31bec83eeeff8c33e539293b824c5c]",
+				"sub_items": [
+				{
+					"migration_id": "g2d182762c23c79eec72fdbfaf4e72f497",
+					"property": "copy[assignments][id_g7510f7e9257131eedbf456fa851ce2727]",
+					"title": "Week 1: Assignment",
+					"type": "assignments"
+				},
+				{
+					"migration_id": "g0d4b9570357bf08230b2b5def78513da0",
+					"property": "copy[assignments][id_g8ed8a7082b0884b2ed9d1adcb2b0610e0]",
+					"title": "Week 2: Assignment",
+					"type": "assignments"
+				}
+				],
+				"title": "Assignments",
+				"type": "assignment_groups"
+			}
+			],
+		"status_code": 200
+	},
+
+	"get_selective_data_assignments_user": {
+		"method": "GET",
+		"endpoint": "users/1/content_migrations/1/selective_data?type=assignments",
+		"data": [
+			{
+				"content_migration_id": 1,
+				"context_id": 1,
+				"context_type": "user",
+				"migration_id": "g8b133a71f8693ea12f549743bf59d194",
+				"property": "copy[assignment_groups][id_ga1f85a92f38ea154ea78bdf34af5a945]",
+				"sub_items": [
+				{
+					"linked_resource": {
+					"message": "linked with Discussion \"Topic Week 1: Discussion\"",
+					"migration_id": "ge1a27af717d06a0782d708c9441be1f68",
+					"property": "copy[discussion_topics][id_g1e4eabe30567ae83bfa62d08b88e56f08]",
+					"type": "discussion_topics"
+					},
+					"migration_id": "g486ff8a1e91a1d9403125c838ea2a75c8",
+					"property": "copy[assignments][id_g4c51f316971e47ac786d5cc1b16d2b198]",
+					"title": "Week 1: Discussion",
+					"type": "assignments"
+				},
+				{
+					"linked_resource": {
+					"message": "linked with Discussion Topic \"Week 2: Discussion\"",
+					"migration_id": "g047d0ed5637242f5623cb932e347ec3c5",
+					"property": "copy[discussion_topics][id_ge517692a894ba8a3e95a2a18655e13905]",
+					"type": "discussion_topics"
+					},
+					"migration_id": "g0d58cddab9615f2226a44c0aa44bf33eb",
+					"property": "copy[assignments][id_g2777dc21adf38a770697313c7ca693e8b]",
+					"title": "Week 2: Discussion",
+					"type": "assignments"
+				}
+				],
+				"title": "Discussion",
+				"type": "assignment_groups"
+			},
+			{
+				"content_migration_id": 1,
+				"context_id": 1,
+				"context_type": "user",
+				"migration_id": "gaba50c7f7eb473769f7dfb649f8ae6bac",
+				"property": "copy[assignment_groups][id_ge9c31bec83eeeff8c33e539293b824c5c]",
+				"sub_items": [
+				{
+					"migration_id": "g2d182762c23c79eec72fdbfaf4e72f497",
+					"property": "copy[assignments][id_g7510f7e9257131eedbf456fa851ce2727]",
+					"title": "Week 1: Assignment",
+					"type": "assignments"
+				},
+				{
+					"migration_id": "g0d4b9570357bf08230b2b5def78513da0",
+					"property": "copy[assignments][id_g8ed8a7082b0884b2ed9d1adcb2b0610e0]",
+					"title": "Week 2: Assignment",
+					"type": "assignments"
+				}
+				],
+				"title": "Assignments",
+				"type": "assignment_groups"
+			}
+			],
+		"status_code": 200
+	},
 	"update_issue": {
 		"method": "PUT",
 		"endpoint": "accounts/1/content_migrations/1/migration_issues/1",
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/eportfolio.json b/tests/fixtures/eportfolio.json
new file mode 100644
index 00000000..95f54cd5
--- /dev/null
+++ b/tests/fixtures/eportfolio.json
@@ -0,0 +1,75 @@
+{
+    "delete_eportfolio": {
+        "method": "DELETE",
+        "endpoint": "eportfolios/1",
+        "data": {
+            "id": 1,
+            "name": "ePortfolio 1",
+            "workflow_state": "deleted",
+            "description": "Delete this ePortfolio",
+            "deleted_at": "2022-07-05T21:00:00Z"
+        },
+        "status_code": 200
+    },
+    "get_eportfolio_by_id": {
+        "method": "GET",
+        "endpoint": "eportfolios/1",
+        "data": {
+            "id": 1,
+            "user_id": 1,
+            "workflow_state": "active",
+            "name": "ePortfolio 1",
+            "deleted_at": "null",
+            "spam_status": "null"
+        },
+        "status_code": 200
+    },
+    "get_eportfolio_pages": {
+        "method": "GET",
+        "endpoint": "eportfolios/1/pages",
+        "data": [
+            {
+                "id": 1,
+                "eportfolio_id": 1,
+                "position": 1,
+                "name": "ePortfolio 1",
+                "content": "This is the page of content",
+                "created_at": "2022-07-01T18:00:00Z",
+                "updated_at": "2021-07-04T18:00:00Z"
+            },
+            {
+                "id": 2,
+                "eportfolio_id": 1,
+                "position": 2,
+                "name": "ePortfolio 1",
+                "content": "This is the second page of content",
+                "created_at": "2022-07-02T18:00:00Z",
+                "updated_at": "2021-07-02T18:00:00Z"
+            }
+        ],
+        "status_code": 200
+    },
+    "moderate_eportfolio_as_spam": {
+        "method": "PUT",
+        "endpoint": "eportfolios/1/moderate",
+        "data": {
+            "id": 1,
+            "user_id": 1,
+            "workflow_state": "active",
+            "name": "ePortfolio 1",
+            "deleted_at": "null",
+            "spam_status": "marked_as_spam"
+        }
+    },
+    "restore_deleted_eportfolio": {
+        "method": "PUT",
+        "endpoint": "eportfolios/1/restore",
+        "data": {
+            "id": 1,
+            "user_id": 1,
+            "workflow_state": "active",
+            "name": "ePortfolio 1",
+            "deleted_at": "null"
+        }
+    }
+}
diff --git a/tests/fixtures/outcome.json b/tests/fixtures/outcome.json
index 4e762942..50a45ec9 100644
--- a/tests/fixtures/outcome.json
+++ b/tests/fixtures/outcome.json
@@ -430,6 +430,23 @@
             "assessed": true
         }
     },
+    "outcome_result_example": {
+        "method": "GET",
+        "endpoint": "courses/1/outcome_results",
+        "data": {
+            "outcome_results": [{
+                "id": 1,
+                "score": 6,
+                "submitted_or_assessed_at": "2013-02-01T00:00:00-06:00",
+                "links": {
+                    "user":"1",
+                    "learning_outcome":"3",
+                    "alignment":"53"
+                },
+                "percent": 0.65
+            }]
+        }
+    },
     "outcome_group_example_account": {
         "method": "GET",
         "endpoint": "accounts/1/outcome_groups/2",
diff --git a/tests/fixtures/rubric.json b/tests/fixtures/rubric.json
index 78f8fcdc..df4e0586 100644
--- a/tests/fixtures/rubric.json
+++ b/tests/fixtures/rubric.json
@@ -16,5 +16,21 @@
 			"association_type": "Assignment"
 		},
 		"status_code": 200
+	},
+	"delete_rubric": {
+		"method": "DELETE",
+		"endpoint": "courses/1/rubrics/1",
+		"data": {
+			"id": 1,
+			"title": "Course Rubric 1",
+			"context_id": 1,
+			"context_type": "Course",
+			"points_possible": 10.0,
+			"reusable": false,
+			"read_only": true,
+			"free_form_critereon_comments": true,
+			"hide_score_total": true
+		},
+		"status_code": 200
 	}
 }
diff --git a/tests/fixtures/submission.json b/tests/fixtures/submission.json
index 1bba6eb2..4f290ec0 100644
--- a/tests/fixtures/submission.json
+++ b/tests/fixtures/submission.json
@@ -51,12 +51,33 @@
 		},
 		"status_code": 200
 	},
+	"get_by_id_with_attachments": {
+		"method": "GET",
+		"endpoint": "courses/1/assignments/1/submissions/1",
+		"data": {
+			"id": 1,
+			"assignment_id": 1,
+			"user_id": 1,
+			"html_url": "https://example.com/courses/1/assignments/1/submissions/1",
+			"submission_type": "online_upload",
+			"attachments": [
+				{
+					"id": 123,
+					"display_name": "attachment.jpeg",
+					"content-type": "image/jpeg",
+					"url": "attachment_file",
+					"size": 3543
+				}
+			]
+		},
+		"status_code": 200
+	},
 	"get_times": {
 		"method": "GET",
 		"endpoint": "courses/1/quizzes/1/submissions/1/time",
 		"data": {
-		    "end_at": "9999-99-99T59:59:59Z",
-		    "time_left": 0
+			"end_at": "9999-99-99T59:59:59Z",
+			"time_left": 0
 		},
 		"status_code": 200
 	},
diff --git a/tests/fixtures/user.json b/tests/fixtures/user.json
index 5c49be59..557bf330 100644
--- a/tests/fixtures/user.json
+++ b/tests/fixtures/user.json
@@ -544,6 +544,24 @@
 		],
 		"status_code": 200
 	},
+	"moderate_user_eportfolios": {
+		"method": "PUT",
+		"endpoint": "users/1/eportfolios",
+		"data": [
+			{
+				"name": "ePortfolio 1",
+				"workflow_state": "active",
+				"created_at": "2022-07-05T21:00:00Z",
+				"spam_status": "marked_as_spam"
+			},
+			{
+				"name": "ePortfolio 2",
+				"workflow_state": "active",
+				"created_at": "2022-07-05T21:00:00Z",
+				"spam_status": "marked_as_spam"
+			}
+		]
+	},
 	"page_views": {
 		"method": "GET",
 		"endpoint": "users/1/page_views",
@@ -735,6 +753,21 @@
 		],
 		"status_code": 200
 	},
+	"list_observers": {
+		"method": "GET",
+		"endpoint": "users/1/observers",
+		"data": [
+			{
+				"id": 3,
+				"name": "User 3"
+			},
+			{
+				"id": 4,
+				"name": "User 4"
+			}
+		],
+		"status_code": 200
+	},
 	"add_observee_with_credentials": {
 		"method": "POST",
 		"endpoint": "users/1/observees",
@@ -824,6 +857,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",
@@ -878,6 +1073,44 @@
 		},
 		"status_code": 200
 	},
+	"get_eportfolios": {
+		"method": "GET",
+		"endpoint": "users/1/eportfolios",
+		"data": [
+			{
+				"name": "ePortfolio 1",
+				"workflow_state": "active",
+				"created_at": "2022-07-05T21:00:00Z"
+			},
+			{
+				"name": "ePortfolio 2",
+				"workflow_state": "active",
+				"created_at": "2022-07-05T21:00:00Z"
+			}
+		]
+	},
+	"get_eportfolios_include_deleted": {
+		"method": "GET",
+		"endpoint": "users/1/eportfolios",
+		"data": [
+			{
+				"name": "ePortfolio 1",
+				"workflow_state": "active",
+				"created_at": "2022-07-05T21:00:00Z"
+			},
+			{
+				"name": "ePortfolio 2",
+				"workflow_state": "active",
+				"created_at": "2022-07-05T21:00:00Z"
+			},
+			{
+				"name": "ePortfolio 3",
+				"workflow_state": "deleted",
+				"created_at": "2022-07-05T21:00:00Z",
+				"deleted_at": "2022-07-06T18:00:00Z"
+			}
+		]
+	},
 	"get_features": {
 		"method": "GET",
 		"endpoint": "users/1/features",
diff --git a/tests/test_account.py b/tests/test_account.py
index 9d3a0d71..ab23f0ac 100644
--- a/tests/test_account.py
+++ b/tests/test_account.py
@@ -152,6 +152,23 @@ def test_delete_root_account(self, m):
         with self.assertRaises(CanvasException):
             self.account.delete()
 
+    # delete_admin()
+    def test_delete_admin(self, m):
+        register_uris({"account": ["delete_admin"]}, m)
+
+        user_id = 123
+        deleted_admin = self.account.delete_admin(user=user_id)
+
+        self.assertIsInstance(deleted_admin, Admin)
+        self.assertTrue(hasattr(deleted_admin, "id"))
+        self.assertTrue(hasattr(deleted_admin, "role"))
+        self.assertTrue(hasattr(deleted_admin, "role_id"))
+        self.assertTrue(hasattr(deleted_admin, "workflow_state"))
+        self.assertEqual(deleted_admin.user["login_id"], "jdoe")
+        self.assertEqual(deleted_admin.role, "AccountAdmin")
+        self.assertEqual(deleted_admin.role_id, 1)
+        self.assertEqual(deleted_admin.workflow_state, "deleted")
+
     # delete_user()
     def test_delete_user_id(self, m):
         register_uris({"account": ["delete_user"]}, m)
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_canvas.py b/tests/test_canvas.py
index 84be6e24..35ece62b 100644
--- a/tests/test_canvas.py
+++ b/tests/test_canvas.py
@@ -14,6 +14,7 @@
 from canvasapi.course import Course, CourseNickname
 from canvasapi.course_epub_export import CourseEpubExport
 from canvasapi.discussion_topic import DiscussionTopic
+from canvasapi.eportfolio import EPortfolio
 from canvasapi.exceptions import RequiredFieldMissing, ResourceDoesNotExist
 from canvasapi.file import File
 from canvasapi.group import Group, GroupCategory
@@ -779,6 +780,15 @@ def test_course_announcements_legacy(self, m):
         self.assertEqual(announcements[0]._parent_type, "course")
         self.assertEqual(announcements[0]._parent_id, "1")
 
+    # get_eportfolio()
+    def test_get_eportfolio(self, m):
+        register_uris({"eportfolio": ["get_eportfolio_by_id"]}, m)
+
+        eportfolio = self.canvas.get_eportfolio(1)
+
+        self.assertIsInstance(eportfolio, EPortfolio)
+        self.assertEqual(eportfolio.name, "ePortfolio 1")
+
     # get_epub_exports()
     def test_get_epub_exports(self, m):
 
diff --git a/tests/test_canvas_object.py b/tests/test_canvas_object.py
index 23f80185..0a3b176c 100644
--- a/tests/test_canvas_object.py
+++ b/tests/test_canvas_object.py
@@ -1,5 +1,64 @@
 import unittest
+from datetime import datetime
 
+import pytz
+import requests_mock
 
+from canvasapi.canvas_object import CanvasObject
+from canvasapi.requester import Requester
+from tests import settings
+
+
+@requests_mock.Mocker()
 class TestCanvasObject(unittest.TestCase):
-    pass
+    def setUp(self):
+        self.canvas_object = CanvasObject(
+            Requester(settings.BASE_URL, settings.API_KEY), {}
+        )
+
+    # set_attributes
+    def test_set_attributes_valid_date(self, m):
+        attributes = {
+            "start_at": "2012-05-05T00:00:00Z",
+            "end_at": "2012-08-05",
+            "offset_time": "2018-05-21T10:22:25+01:00",
+            "half_offset": "2018-05-21T13:52:25+04:30",
+            "big_offset_time": "2018-05-21T23:22:25+14:00",
+            "big_offset_neg": "2018-05-20T23:22:25-10:00",
+        }
+
+        start_date = datetime.strptime(
+            attributes["start_at"], "%Y-%m-%dT%H:%M:%SZ"
+        ).replace(tzinfo=pytz.utc)
+        end_date = datetime.strptime(attributes["end_at"], "%Y-%m-%d").replace(
+            tzinfo=pytz.utc
+        )
+        offset_time = datetime.strptime(
+            "2018-05-21T09:22:25Z", "%Y-%m-%dT%H:%M:%SZ"
+        ).replace(tzinfo=pytz.utc)
+
+        self.canvas_object.set_attributes(attributes)
+
+        self.assertTrue(hasattr(self.canvas_object, "start_at_date"))
+        self.assertEqual(self.canvas_object.start_at_date, start_date)
+        self.assertTrue(hasattr(self.canvas_object, "end_at_date"))
+        self.assertEqual(self.canvas_object.end_at_date, end_date)
+        self.assertTrue(hasattr(self.canvas_object, "offset_time_date"))
+        self.assertEqual(self.canvas_object.offset_time_date, offset_time)
+        self.assertTrue(hasattr(self.canvas_object, "big_offset_time_date"))
+        self.assertEqual(self.canvas_object.big_offset_time_date, offset_time)
+        self.assertTrue(hasattr(self.canvas_object, "big_offset_neg_date"))
+        self.assertEqual(self.canvas_object.big_offset_neg_date, offset_time)
+
+        self.assertTrue(hasattr(self.canvas_object, "half_offset_date"))
+        self.assertEqual(self.canvas_object.half_offset_date, offset_time)
+
+    def test_set_attributes_invalid_date(self, m):
+        attributes = {"start_at": "2017-01-01T00:00+00:00:00", "end_at": "2012-08-0"}
+
+        self.canvas_object.set_attributes(attributes)
+
+        self.assertFalse(hasattr(self.canvas_object, "start_at_date"))
+        self.assertFalse(hasattr(self.canvas_object, "end_at_date"))
+        self.assertTrue(hasattr(self.canvas_object, "start_at"))
+        self.assertTrue(hasattr(self.canvas_object, "end_at"))
diff --git a/tests/test_content_migration.py b/tests/test_content_migration.py
index 553d0ae6..bada70e6 100644
--- a/tests/test_content_migration.py
+++ b/tests/test_content_migration.py
@@ -4,7 +4,11 @@
 
 from canvasapi import Canvas
 from canvasapi.account import Account
-from canvasapi.content_migration import ContentMigration, MigrationIssue
+from canvasapi.content_migration import (
+    ContentMigration,
+    ContentMigrationSelectionNode,
+    MigrationIssue,
+)
 from canvasapi.course import Course
 from canvasapi.group import Group
 from canvasapi.progress import Progress
@@ -256,3 +260,50 @@ def setUp(self):
     def test__str__(self, m):
         string = str(self.migrator)
         self.assertIsInstance(string, str)
+
+
+@requests_mock.Mocker()
+class TestSelectiveData(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"],
+                "group": ["get_by_id"],
+                "account": ["get_by_id"],
+                "user": ["get_by_id"],
+            }
+            register_uris(requires, m)
+
+            self.account = self.canvas.get_account(1)
+            self.course = self.canvas.get_course(1)
+            self.group = self.canvas.get_group(1)
+            self.user = self.canvas.get_user(1)
+
+    def test_get_selective_data(self, m):
+        register_uris(
+            {
+                "course": ["get_content_migration_single"],
+                "group": ["get_content_migration_single"],
+                "account": ["get_content_migration_single"],
+                "user": ["get_content_migration_single"],
+                "content_migration": [
+                    "get_selective_data_account",
+                    "get_selective_data_course",
+                    "get_selective_data_group",
+                    "get_selective_data_user",
+                ],
+            },
+            m,
+        )
+        for context_type in ["account", "course", "group", "user"]:
+            context = getattr(self, context_type)
+            migration = context.get_content_migration(1)
+            for node in migration.get_selective_data():
+                self.assertIsInstance(node, ContentMigrationSelectionNode)
+                self.assertIsInstance(str(node), str)
+
+            for node in migration.get_selective_data(type="assignments"):
+                self.assertIsInstance(node, ContentMigrationSelectionNode)
+                self.assertIsInstance(str(node), str)
diff --git a/tests/test_course.py b/tests/test_course.py
index 8f958298..b9a3daef 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,
@@ -33,7 +34,7 @@
 from canvasapi.group import Group, GroupCategory
 from canvasapi.license import License
 from canvasapi.module import Module
-from canvasapi.outcome import OutcomeGroup, OutcomeLink
+from canvasapi.outcome import OutcomeGroup, OutcomeLink, OutcomeResult
 from canvasapi.outcome_import import OutcomeImport
 from canvasapi.paginated_list import PaginatedList
 from canvasapi.progress import Progress
@@ -1026,8 +1027,7 @@ def test_get_course_level_student_summary_data(self, m):
         register_uris({"course": ["get_course_level_student_summary_data"]}, m)
 
         response = self.course.get_course_level_student_summary_data()
-
-        self.assertIsInstance(response, list)
+        self.assertIsInstance(response, PaginatedList)
 
     # get_user_in_a_course_level_participation_data()
     def test_get_user_in_a_course_level_participation_data(self, m):
@@ -1202,6 +1202,7 @@ def test_get_rubric(self, m):
         self.assertIsInstance(rubric, Rubric)
         self.assertEqual(rubric.id, rubric_id)
         self.assertEqual(rubric.title, "Course Rubric 1")
+        self.assertTrue(hasattr(rubric, "course_id"))
 
     # get_rubrics
     def test_get_rubrics(self, m):
@@ -1214,9 +1215,12 @@ def test_get_rubrics(self, m):
         self.assertIsInstance(rubrics[0], Rubric)
         self.assertEqual(rubrics[0].id, 1)
         self.assertEqual(rubrics[0].title, "Course Rubric 1")
+        self.assertTrue(hasattr(rubrics[0], "course_id"))
+
         self.assertIsInstance(rubrics[1], Rubric)
         self.assertEqual(rubrics[1].id, 2)
         self.assertEqual(rubrics[1].title, "Course Rubric 2")
+        self.assertTrue(hasattr(rubrics[1], "course_id"))
 
     # get_root_outcome_group()
     def test_get_root_outcome_group(self, m):
@@ -1267,9 +1271,10 @@ def test_get_outcome_results(self, m):
         register_uris({"outcome": ["course_get_outcome_results"]}, m)
 
         result = self.course.get_outcome_results()
+        outcome_results = [item for item in result]
 
-        self.assertIsInstance(result, dict)
-        self.assertIsInstance(result["outcome_results"], list)
+        self.assertIsInstance(result, PaginatedList)
+        self.assertIsInstance(outcome_results[0], OutcomeResult)
 
     # get_outcome_result_rollups()
     def test_get_outcome_result_rollups(self, m):
@@ -1600,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)
@@ -1794,6 +1813,27 @@ def test_remove(self, m):
         self.assertTrue(hasattr(deleted_nick, "nickname"))
 
 
+@requests_mock.Mocker()
+class TestCourseStudentSummary(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", "get_course_level_student_summary_data"]}, m
+            )
+
+            self.course = self.canvas.get_course(1)
+            self.course_student_summary = (
+                self.course.get_course_level_student_summary_data()[0]
+            )
+
+    # __str__()
+    def test__str__(self, m):
+        string = str(self.course_student_summary)
+        self.assertIsInstance(string, str)
+
+
 @requests_mock.Mocker()
 class TestLatePolicy(unittest.TestCase):
     def setUp(self):
diff --git a/tests/test_eportfolio.py b/tests/test_eportfolio.py
new file mode 100644
index 00000000..92b71aba
--- /dev/null
+++ b/tests/test_eportfolio.py
@@ -0,0 +1,73 @@
+import unittest
+
+import requests_mock
+
+from canvasapi import Canvas
+from canvasapi.eportfolio import EPortfolio, EPortfolioPage
+from canvasapi.paginated_list import PaginatedList
+from tests import settings
+from tests.util import register_uris
+
+
+@requests_mock.Mocker()
+class TestEPortfolio(unittest.TestCase):
+    def setUp(self):
+        self.canvas = Canvas(settings.BASE_URL, settings.API_KEY)
+
+        with requests_mock.Mocker() as m:
+            register_uris({"eportfolio": ["get_eportfolio_by_id"]}, m)
+
+            self.eportfolio = self.canvas.get_eportfolio(1)
+
+    def test_str(self, m):
+        eportfolio_string = str(self.eportfolio)
+        self.assertEqual(eportfolio_string, "ePortfolio 1")
+
+    def test_delete_eportfolio(self, m):
+        register_uris({"eportfolio": ["delete_eportfolio"]}, m)
+
+        deleted_eportfolio = self.eportfolio.delete()
+
+        self.assertIsInstance(deleted_eportfolio, EPortfolio)
+        self.assertEqual(deleted_eportfolio.deleted_at, "2022-07-05T21:00:00Z")
+        self.assertEqual(deleted_eportfolio.workflow_state, "deleted")
+
+    def test_get_eportfolio_pages(self, m):
+        register_uris({"eportfolio": ["get_eportfolio_pages"]}, m)
+
+        pages = self.eportfolio.get_eportfolio_pages()
+
+        string_page = str(pages[0])
+
+        self.assertIsInstance(pages, PaginatedList)
+        self.assertIsInstance(pages[0], EPortfolioPage)
+        self.assertIsInstance(pages[1], EPortfolioPage)
+        self.assertEqual(pages[0].position, 1)
+        self.assertEqual(pages[1].position, 2)
+        self.assertEqual(string_page, "1. ePortfolio 1")
+
+    def test_moderate_eportfolio_as_spam(self, m):
+        register_uris({"eportfolio": ["moderate_eportfolio_as_spam"]}, m)
+
+        spam_eportfolio = self.eportfolio.moderate_eportfolio(
+            spam_status="marked_as_spam"
+        )
+
+        self.assertIsInstance(spam_eportfolio, EPortfolio)
+        self.assertEqual(spam_eportfolio.spam_status, "marked_as_spam")
+
+    def test_restore_deleted_eportfolio(self, m):
+        register_uris(
+            {"eportfolio": ["delete_eportfolio", "restore_deleted_eportfolio"]}, m
+        )
+
+        eportfolio = self.eportfolio.delete()
+
+        self.assertIsInstance(eportfolio, EPortfolio)
+        self.assertEqual(eportfolio.workflow_state, "deleted")
+        self.assertEqual(eportfolio.deleted_at, "2022-07-05T21:00:00Z")
+
+        restored = eportfolio.restore()
+
+        self.assertEqual(restored.workflow_state, "active")
+        self.assertEqual(restored.deleted_at, "null")
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_outcome.py b/tests/test_outcome.py
index 0b52896f..f6f85007 100644
--- a/tests/test_outcome.py
+++ b/tests/test_outcome.py
@@ -481,3 +481,28 @@ def test_import_outcome_group(self, m):
         self.assertEqual(
             result_by_obj.parent_outcome_group["title"], self.course_outcome_group.title
         )
+
+
+@requests_mock.Mocker()
+class TestOutcomeResult(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"],
+                    "outcome": ["outcome_example", "outcome_result_example"],
+                },
+                m,
+            )
+
+            self.course = self.canvas.get_course(1)
+            self.course_outcome_results = self.course.get_outcome_results()
+            self.outcome_result_example = self.course_outcome_results[0]
+            # self.example_outcome = self.course_outcome_links[0].get_outcome()
+
+    # __str__()
+    def test__str__(self, m):
+        string = str(self.outcome_result_example)
+        self.assertIsInstance(string, str)
diff --git a/tests/test_quiz.py b/tests/test_quiz.py
index 45049f4e..94ae9454 100644
--- a/tests/test_quiz.py
+++ b/tests/test_quiz.py
@@ -461,7 +461,7 @@ def test_get_submission_events(self, m):
         register_uris({"quiz": ["get_submission_events"]}, m)
 
         events = self.submission.get_submission_events()
-        self.assertIsInstance(events, list)
+        self.assertIsInstance(events, PaginatedList)
         self.assertIsInstance(events[0], QuizSubmissionEvent)
         self.assertIsInstance(events[1], QuizSubmissionEvent)
         self.assertEqual(str(events[0]), "page_blurred")
@@ -515,7 +515,7 @@ def test_submit_events(self, m):
 
         test_events = self.submission.get_submission_events()
 
-        result = self.submission.submit_events(test_events)
+        result = self.submission.submit_events(list(test_events))
         self.assertTrue(result)
 
     def test_submit_events_fail(self, m):
diff --git a/tests/test_quiz_group.py b/tests/test_quiz_group.py
index cd38b73b..bb800804 100644
--- a/tests/test_quiz_group.py
+++ b/tests/test_quiz_group.py
@@ -31,7 +31,7 @@ def test_update(self, m):
         register_uris({"quiz_group": ["update"]}, m)
 
         quiz_group = [{"name": "Test Group", "pick_count": 1, "question_points": 2}]
-        result = self.quiz_group.update(10, quiz_group)
+        result = self.quiz_group.update(quiz_group)
 
         self.assertIsInstance(result, bool)
         self.assertTrue(result)
@@ -50,7 +50,7 @@ def test_update_empty_list(self, m):
         quiz_group = []
 
         with self.assertRaises(ValueError):
-            self.quiz_group.update(10, quiz_group)
+            self.quiz_group.update(quiz_group)
 
     def test_update_incorrect_param(self, m):
         register_uris({"quiz_group": ["update"]}, m)
@@ -58,7 +58,7 @@ def test_update_incorrect_param(self, m):
         quiz_group = [1]
 
         with self.assertRaises(ValueError):
-            self.quiz_group.update(10, quiz_group)
+            self.quiz_group.update(quiz_group)
 
     def test_update_incorrect_dict(self, m):
         register_uris({"quiz_group": ["update"]}, m)
@@ -66,13 +66,13 @@ def test_update_incorrect_dict(self, m):
         quiz_group = [{}]
 
         with self.assertRaises(RequiredFieldMissing):
-            self.quiz_group.update(10, quiz_group)
+            self.quiz_group.update(quiz_group)
 
     # delete_question_group()
     def test_delete(self, m):
         register_uris({"quiz_group": ["delete"]}, m)
 
-        result = self.quiz_group.delete(10)
+        result = self.quiz_group.delete()
 
         self.assertTrue(result)
 
@@ -81,7 +81,7 @@ def test_reorder_question_group(self, m):
         register_uris({"quiz_group": ["reorder_question_group"]}, m)
 
         newOrdering = [{"id": 2}, {"id": 1, "type": "question"}]
-        result = self.quiz_group.reorder_question_group(10, newOrdering)
+        result = self.quiz_group.reorder_question_group(newOrdering)
 
         self.assertTrue(result)
 
@@ -91,7 +91,7 @@ def test_reorderquestion_group_empty_list(self, m):
         order = []
 
         with self.assertRaises(ValueError):
-            self.quiz_group.reorder_question_group(10, order)
+            self.quiz_group.reorder_question_group(order)
 
     def test_reorderquestion_group_incorrect_param(self, m):
         register_uris({"quiz_group": ["reorder_question_group"]}, m)
@@ -99,7 +99,7 @@ def test_reorderquestion_group_incorrect_param(self, m):
         order = [1]
 
         with self.assertRaises(ValueError):
-            self.quiz_group.reorder_question_group(10, order)
+            self.quiz_group.reorder_question_group(order)
 
     def test_reorderquestion_group_incorrect_dict(self, m):
         register_uris({"quiz_group": ["reorder_question_group"]}, m)
@@ -107,4 +107,4 @@ def test_reorderquestion_group_incorrect_dict(self, m):
         order = [{"something": 2}]
 
         with self.assertRaises(ValueError):
-            self.quiz_group.reorder_question_group(10, order)
+            self.quiz_group.reorder_question_group(order)
diff --git a/tests/test_rubric.py b/tests/test_rubric.py
index 626adb5d..6735225e 100644
--- a/tests/test_rubric.py
+++ b/tests/test_rubric.py
@@ -3,11 +3,45 @@
 import requests_mock
 
 from canvasapi import Canvas
-from canvasapi.rubric import RubricAssociation
+from canvasapi.rubric import Rubric, RubricAssociation
 from tests import settings
 from tests.util import register_uris
 
 
+@requests_mock.Mocker()
+class TestRubric(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", "create_rubric"]}, m)
+
+            self.course = self.canvas.get_course(1)
+            self.rubric = self.course.create_rubric()["rubric"]
+
+    # delete
+    def test_delete(self, m):
+        register_uris({"rubric": ["delete_rubric"]}, m)
+
+        rubric = self.rubric.delete()
+
+        self.assertIsInstance(rubric, Rubric)
+        self.assertEqual(rubric.id, 1)
+        self.assertEqual(rubric.title, "Course Rubric 1")
+        self.assertEqual(rubric.context_id, 1)
+        self.assertEqual(rubric.context_type, "Course")
+        self.assertEqual(rubric.points_possible, 10.0)
+        self.assertFalse(rubric.reusable)
+        self.assertTrue(rubric.read_only)
+        self.assertTrue(rubric.free_form_critereon_comments)
+        self.assertTrue(rubric.hide_score_total)
+
+    # __str__()
+    def test__str__(self, m):
+        string = str(self.rubric)
+        self.assertIsInstance(string, str)
+
+
 @requests_mock.Mocker()
 class TestGradingStandard(unittest.TestCase):
     def setUp(self):
diff --git a/tests/test_submission.py b/tests/test_submission.py
index 64950658..cdcb5953 100644
--- a/tests/test_submission.py
+++ b/tests/test_submission.py
@@ -4,6 +4,7 @@
 import requests_mock
 
 from canvasapi import Canvas
+from canvasapi.file import File
 from canvasapi.peer_review import PeerReview
 from canvasapi.submission import GroupedSubmission, Submission
 from tests import settings
@@ -30,6 +31,19 @@ def setUp(self):
             self.assignment = self.course.get_assignment(1)
             self.submission = self.assignment.get_submission(1)
 
+    # __init__()
+    def test__init__attachments(self, m):
+        register_uris({"submission": ["get_by_id_with_attachments"]}, m)
+
+        submission = self.assignment.get_submission(1)
+
+        self.assertTrue(hasattr(submission, "attachments"))
+        self.assertIsInstance(submission.attachments, list)
+        self.assertEqual(len(submission.attachments), 1)
+        self.assertIsInstance(submission.attachments[0], File)
+        self.assertTrue(hasattr(submission.attachments[0], "id"))
+        self.assertEqual(submission.attachments[0].id, 123)
+
     # __str__()
     def test__str__(self, m):
         string = str(self.submission)
diff --git a/tests/test_user.py b/tests/test_user.py
index 2234e0ad..9093b783 100644
--- a/tests/test_user.py
+++ b/tests/test_user.py
@@ -13,9 +13,11 @@
 from canvasapi.content_migration import ContentMigration, Migrator
 from canvasapi.course import Course
 from canvasapi.enrollment import Enrollment
+from canvasapi.eportfolio import EPortfolio
 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 +74,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)
@@ -250,6 +280,40 @@ def test_create_communication_channels(self, m):
 
         self.assertIsInstance(new_channel, CommunicationChannel)
 
+    # get_eportfolios()
+    def test_get_eportfolios(self, m):
+        register_uris({"user": ["get_eportfolios"]}, m)
+
+        eportfolios = self.user.get_eportfolios()
+        eportfolio_list = [portfolio for portfolio in eportfolios]
+        self.assertIsInstance(eportfolios, PaginatedList)
+        self.assertIsInstance(eportfolios[0], EPortfolio)
+        self.assertEqual(len(eportfolio_list), 2)
+        self.assertEqual(eportfolios[0].name, "ePortfolio 1")
+
+    def test_get_eportfolios_with_deleted(self, m):
+        register_uris({"user": ["get_eportfolios_include_deleted"]}, m)
+
+        eportfolios = self.user.get_eportfolios()
+        eportfolio_list = [portfolio for portfolio in eportfolios]
+        self.assertIsInstance(eportfolios, PaginatedList)
+        self.assertIsInstance(eportfolios[0], EPortfolio)
+        self.assertEqual(len(eportfolio_list), 3)
+        self.assertEqual(eportfolios[2].name, "ePortfolio 3")
+        self.assertEqual(eportfolios[2].workflow_state, "deleted")
+
+    def test_moderate_user_eportfolios(self, m):
+        register_uris({"user": ["moderate_user_eportfolios"]}, m)
+
+        spam_eportfolios = self.user.moderate_all_eportfolios(
+            spam_status="marked_as_spam"
+        )
+        eportfolio_list = [portfolio for portfolio in spam_eportfolios]
+        self.assertIsInstance(spam_eportfolios, PaginatedList)
+        self.assertIsInstance(spam_eportfolios[0], EPortfolio)
+        self.assertEqual(len(eportfolio_list), 2)
+        self.assertEqual(spam_eportfolios[0].spam_status, "marked_as_spam")
+
     # get_files()
     def test_get_files(self, m):
         register_uris({"user": ["get_user_files", "get_user_files2"]}, m)
@@ -333,6 +397,16 @@ def test_get_observees(self, m):
         self.assertIsInstance(observees_list[0], User)
         self.assertEqual(len(observees_list), 4)
 
+    def test_get_observers(self, m):
+        requires = {"user": ["list_observers", "list_observers"]}
+        register_uris(requires, m)
+
+        response = self.user.get_observers()
+        observers_list = [observers for observers in response]
+
+        self.assertIsInstance(observers_list[0], User)
+        self.assertEqual(len(observers_list), 2)
+
     # add_observee_with_credentials()
     def test_add_observee_with_credentials(self, m):
         register_uris({"user": ["add_observee_with_credentials"]}, m)
diff --git a/tests_requirements.txt b/tests_requirements.txt
index ecc22e14..3901adee 100644
--- a/tests_requirements.txt
+++ b/tests_requirements.txt
@@ -1,6 +1,6 @@
 -r requirements.txt
 
-black; python_version >= '3.6'
+black
 coverage
 flake8
 isort