Repository: ucfopen/canvasapi
Pull request #344 (2019-11-19 16:43:53)

Release v0.15.0
🎉


Patch:
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 587e43b..dc84e02 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -14,7 +14,7 @@ Below you'll find guidelines for contributing that will keep our codebase clean
         * [Writing tests](#writing-tests)
             * [API Coverage Tests](#api-coverage-tests)
             * [Engine tests](#engine-tests)
-        * [Running tests / coverage reports](#running-tests-coverage-reports)
+        * [Running tests / coverage reports](#running-tests--coverage-reports)
         * [Making a Pull Request](#making-a-pull-request)
 * [Code style guidelines](#code-style-guidelines)
     * [Foolish consistency](#foolish-consistency)
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 9e909fb..25678a5 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -24,7 +24,9 @@ Steps to reproduce the behavior:
 
 A clear and concise description of what you expected to happen.
 
-# Environment information (please complete the following information)
+# Environment information
+
+<!--(please complete the following information)-->
 
 - Python version (`python --version`)
 - CanvasAPI version (`pip show canvasapi`)
diff --git a/AUTHORS.md b/AUTHORS.md
index 6584013..8baa6be 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -18,6 +18,8 @@ Patches and Suggestions
 - Bill Wrbican [@wjw27](https://github.com/wjw27)
 - Bradford Lynch [@bradfordlynch](https://github.com/bradfordlynch)
 - Catherine Abbruzzese [@cat0698](https://github.com/cat0698))
+- Cameron Cuff [@ctcuff](https://github.com/ctcuff)
+- Dalton Durst [@UniversalSuperBox](https://github.com/UniversalSuperBox)
 - Daniel Brinkman [@DanBrink91](https://github.com/DanBrink91)
 - Daniel Grobani [@dgrobani](https://github.com/dgrobani)
 - David Warden [@dfwarden](https://github.com/dfwarden)
@@ -25,16 +27,19 @@ Patches and Suggestions
 - Devin Singh [@devints47](https://github.com/devints47)
 - Elise Heron [@thedarkestknight](https://github.com/thedarkestknight)
 - Elli Howard [@qwertynerd97](https://github.com/qwertynerd97)
+- Erik Tews [@eriktews](https://github.com/eriktews)
 - Ethan Finlay [@atarisafari](https://github.com/atarisafari)
 - Gabriela Dijkhoffz [@gdijkhoffz](https://github.com/gdijkhoffz)
 - Henry Acevedo [@Colombiangmr](https://github.com/Colombiangmr)
 - Ian Altgilbers [@altgilbers](https://github.com/altgilbers)
 - Ian Turgeon [@iturgeon](https://github.com/iturgeon)
 - [@jackrsteiner](https://github.com/jackrsteiner)
+- Joon Ro [@joonro](https://github.com/joonro)
 - Jonathan Guilbe [@JonGuilbe](https://github.com/JonGuilbe)
 - John Raible [@rebelaide](https://github.com/rebelaide)
 - Jose Silveti [@jrsilveti](https://github.com/jrsilveti)
 - [@kensler](https://github.com/kensler)
+- Lawrence Oks [@ljoks](https://github.com/ljoks)
 - Mark Lalor [@MarkLalor](https://github.com/MarkLalor)
 - Markus [@elec3647](https://github.com/elec3647)
 - Matthew Fedder [@matthewf-ucsd](https://github.com/matthewf-ucsd)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0ce69fc..ccfacf6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,40 @@
 # Change Log
 
+## [0.15.0] - 2019-11-19
+
+### New Endpoint Coverage
+
+- Assignment Extensions (Thanks, [@ljoks](https://github.com/ljoks))
+- AssignmentGroup (Thanks, [@ctcuff](https://github.com/ctcuff))
+    - List Assignments
+- Authentications Log (Thanks, [@weining-li](https://github.com/weining-li))
+- Brand Configs (Thanks, [@bennettscience](https://github.com/bennettscience))
+- Comm Messages (Thanks, [@ljoks](https://github.com/ljoks))
+- File Usage Rights (Thanks, [@atarisafari](https://github.com/atarisafari) and [@joonro](https://github.com/joonro))
+- Gradebook History (Thanks, [@gdijkhoffz](https://github.com/gdijkhoffz))
+- Quiz Reports (Thanks, [@atarisafari](https://github.com/atarisafari)
+- Quiz Submission Events (Thanks, [@Goff-Davis](https://github.com/Goff-Davis))
+- Quiz Submission User List (Thanks, [@gdijkhoffz](https://github.com/gdijkhoffz))
+- Rubric Associations (Thanks, [@weining-li](https://github.com/weining-li))
+
+### General
+
+- Throw `IndexError` when using negative indexes on `PaginatedList` objects (Thanks, [@UniversalSuperBox](https://github.com/UniversalSuperBox))
+- `Assignment.overrides` now returns a list of `AssignmentOverride` objects.
+
+### Deprecation Warnings
+
+- `CanvasObject.attributes` is now deprecated and will be removed in a future version.
+- `CanvasObject.to_json()` is now deprecated and will be removed in a future version. To view the original attributes sent by Canvas, enable logs from the requests library.
+
+### Bugfixes
+
+- Fixed an issue where `util.clean_headers()` would throw a `ValueError` if a user accidentally included a space in their API token. (Thanks, [@keeeeeegan](https://github.com/keeeeeegan))
+- Fixed an issue where `QuizSubmission` objects sometimes wouldn't have a course_id, making some methods unusable. (Thanks, [@bennettscience](https://github.com/bennettscience))
+- Fixed an issue where `get_user()` did not accept arbitrary keyword arguments (Thanks, [@eriktews](https://github.com/eriktews))
+- Fixed an issue where an import was triggering a `DeprecationWarning` (Thanks, [@Screeeech](https://github.com/Screeeech))
+- Fixed an issue where a GroupedSubmission wasn't saving the `submissions` attribute properly
+
 ## [0.14.0] - 2019-08-20
 
 ### New Endpoint Coverage
@@ -368,6 +403,7 @@ Huge thanks to [@liblit](https://github.com/liblit) for lots of issues, suggesti
 - Fixed some incorrectly defined parameters
 - Fixed an issue where tests would fail due to an improperly configured requires block
 
+[0.15.0]: https://github.com/ucfopen/canvasapi/compare/v0.14.0...v0.15.0
 [0.14.0]: https://github.com/ucfopen/canvasapi/compare/v0.13.0...v0.14.0
 [0.13.0]: https://github.com/ucfopen/canvasapi/compare/v0.12.0...v0.13.0
 [0.12.0]: https://github.com/ucfopen/canvasapi/compare/v0.11.0...v0.12.0
diff --git a/canvasapi/__init__.py b/canvasapi/__init__.py
index ce7ccf6..5dd4b6f 100644
--- a/canvasapi/__init__.py
+++ b/canvasapi/__init__.py
@@ -6,4 +6,4 @@ from canvasapi.canvas import Canvas
 
 __all__ = ["Canvas"]
 
-__version__ = "0.14.0"
+__version__ = "0.15.0"
diff --git a/canvasapi/account.py b/canvasapi/account.py
index d9b5524..d03cea3 100644
--- a/canvasapi/account.py
+++ b/canvasapi/account.py
@@ -642,6 +642,26 @@ class Account(CanvasObject):
             "accounts/{}/outcome_group_links".format(self.id),
         )
 
+    def get_authentication_events(self, **kwargs):
+        """
+        List authentication events for a given account.
+
+        :calls: `GET /api/v1/audit/authentication/accounts/:account_id \
+        <https://canvas.instructure.com/doc/api/authentications_log.html#method.authentication_audit_api.for_account>`_
+
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+                :class:`canvasapi.authentication_event.AuthenticationEvent`
+        """
+        from canvasapi.authentication_event import AuthenticationEvent
+
+        return PaginatedList(
+            AuthenticationEvent,
+            self._requester,
+            "GET",
+            "audit/authentication/accounts/{}".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
     def get_authentication_provider(self, authentication_provider, **kwargs):
         """
         Get the specified authentication provider
diff --git a/canvasapi/assignment.py b/canvasapi/assignment.py
index cd549ca..93f413e 100644
--- a/canvasapi/assignment.py
+++ b/canvasapi/assignment.py
@@ -16,6 +16,15 @@ from canvasapi.util import combine_kwargs, obj_or_id
 
 @python_2_unicode_compatible
 class Assignment(CanvasObject):
+    def __init__(self, requester, attributes):
+        super(Assignment, self).__init__(requester, attributes)
+
+        if "overrides" in attributes:
+            self.overrides = [
+                AssignmentOverride(requester, override)
+                for override in attributes["overrides"]
+            ]
+
     def __str__(self):
         return "{} ({})".format(self.name, self.id)
 
@@ -202,6 +211,75 @@ class Assignment(CanvasObject):
             _kwargs=combine_kwargs(**kwargs),
         )
 
+    def set_extensions(self, assignment_extensions, **kwargs):
+        """
+        Set extensions for student assignment submissions
+
+        :calls: `POST /api/v1/courses/:course_id/assignments/:assignment_id/extensions \
+        <https://canvas.instructure.com/doc/api/assignment_extensions.html#method.assignment_extensions.create>`_
+
+        :param assignment_extensions: list of dictionaries representing extensions
+        :type assignment_extensions: list
+
+        :rtype: list of :class:`canvasapi.assignment.AssignmentExtension`
+
+        Example Usage:
+
+        >>> assignment.set_extensions([
+        ...     {
+        ...         'user_id': 3,
+        ...         'extra_attempts: 2
+        ...     },
+        ...     {
+        ...         'user_id': 2,
+        ...         'extra_attempts: 2
+        ...     }
+        ... ])
+        """
+        if not isinstance(assignment_extensions, list) or not assignment_extensions:
+            raise ValueError("Param `assignment_extensions` must be a non-empty list.")
+
+        if any(not isinstance(extension, dict) for extension in assignment_extensions):
+            raise ValueError(
+                "Param `assignment_extensions` must only contain dictionaries"
+            )
+
+        if any("user_id" not in extension for extension in assignment_extensions):
+            raise RequiredFieldMissing(
+                "Dictionaries in `assignment_extensions` must contain key `user_id`"
+            )
+        kwargs["assignment_extensions"] = assignment_extensions
+        response = self._requester.request(
+            "POST",
+            "courses/{}/assignments/{}/extensions".format(self.course_id, self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+        extension_list = response.json()["assignment_extensions"]
+        return [
+            AssignmentExtension(self._requester, extension)
+            for extension in extension_list
+        ]
+
+    def submissions_bulk_update(self, **kwargs):
+        """
+        Update the grading and comments on multiple student's assignment
+        submissions in an asynchronous job.
+
+        :calls: `POST /api/v1/courses/:course_id/assignments/:assignment_id/ \
+            submissions/update_grades \
+        <https://canvas.instructure.com/doc/api/submissions.html#method.submissions_api.bulk_update>`_
+
+        :rtype: :class:`canvasapi.progress.Progress`
+        """
+        response = self._requester.request(
+            "POST",
+            "courses/{}/assignments/{}/submissions/update_grades".format(
+                self.course_id, self.id
+            ),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+        return Progress(self._requester, response.json())
+
     def submit(self, submission, file=None, **kwargs):
         """
         Makes a submission for an assignment.
@@ -246,26 +324,6 @@ class Assignment(CanvasObject):
 
         return Submission(self._requester, response_json)
 
-    def submissions_bulk_update(self, **kwargs):
-        """
-        Update the grading and comments on multiple student's assignment
-        submissions in an asynchronous job.
-
-        :calls: `POST /api/v1/courses/:course_id/assignments/:assignment_id/ \
-            submissions/update_grades \
-        <https://canvas.instructure.com/doc/api/submissions.html#method.submissions_api.bulk_update>`_
-
-        :rtype: :class:`canvasapi.progress.Progress`
-        """
-        response = self._requester.request(
-            "POST",
-            "courses/{}/assignments/{}/submissions/update_grades".format(
-                self.course_id, self.id
-            ),
-            _kwargs=combine_kwargs(**kwargs),
-        )
-        return Progress(self._requester, response.json())
-
     def upload_to_submission(self, file, user="self", **kwargs):
         """
         Upload a file to a submission.
@@ -296,45 +354,51 @@ class Assignment(CanvasObject):
         ).start()
 
 
+@python_2_unicode_compatible
+class AssignmentExtension(CanvasObject):
+    def __str__(self):
+        return "{} ({})".format(self.assignment_id, self.user_id)
+
+
 @python_2_unicode_compatible
 class AssignmentGroup(CanvasObject):
     def __str__(self):
         return "{} ({})".format(self.name, self.id)
 
-    def edit(self, **kwargs):
+    def delete(self, **kwargs):
         """
-        Modify this assignment group.
+        Delete this assignment.
 
-        :calls: `PUT /api/v1/courses/:course_id/assignment_groups/:assignment_group_id \
-        <https://canvas.instructure.com/doc/api/assignment_groups.html#method.assignment_groups_api.update>`_
+        :calls: `DELETE /api/v1/courses/:course_id/assignment_groups/:assignment_group_id \
+        <https://canvas.instructure.com/doc/api/assignment_groups.html#method.assignment_groups_api.destroy>`_
 
         :rtype: :class:`canvasapi.assignment.AssignmentGroup`
         """
         response = self._requester.request(
-            "PUT",
+            "DELETE",
             "courses/{}/assignment_groups/{}".format(self.course_id, self.id),
             _kwargs=combine_kwargs(**kwargs),
         )
-
-        if "name" in response.json():
-            super(AssignmentGroup, self).set_attributes(response.json())
-
         return AssignmentGroup(self._requester, response.json())
 
-    def delete(self, **kwargs):
+    def edit(self, **kwargs):
         """
-        Delete this assignment.
+        Modify this assignment group.
 
-        :calls: `DELETE /api/v1/courses/:course_id/assignment_groups/:assignment_group_id \
-        <https://canvas.instructure.com/doc/api/assignment_groups.html#method.assignment_groups_api.destroy>`_
+        :calls: `PUT /api/v1/courses/:course_id/assignment_groups/:assignment_group_id \
+        <https://canvas.instructure.com/doc/api/assignment_groups.html#method.assignment_groups_api.update>`_
 
         :rtype: :class:`canvasapi.assignment.AssignmentGroup`
         """
         response = self._requester.request(
-            "DELETE",
+            "PUT",
             "courses/{}/assignment_groups/{}".format(self.course_id, self.id),
             _kwargs=combine_kwargs(**kwargs),
         )
+
+        if "name" in response.json():
+            super(AssignmentGroup, self).set_attributes(response.json())
+
         return AssignmentGroup(self._requester, response.json())
 
 
@@ -358,6 +422,7 @@ class AssignmentOverride(CanvasObject):
             "courses/{}/assignments/{}/overrides/{}".format(
                 self.course_id, self.assignment_id, self.id
             ),
+            _kwargs=combine_kwargs(**kwargs),
         )
 
         response_json = response.json()
@@ -381,6 +446,7 @@ class AssignmentOverride(CanvasObject):
             "courses/{}/assignments/{}/overrides/{}".format(
                 self.course_id, self.assignment_id, self.id
             ),
+            _kwargs=combine_kwargs(**kwargs),
         )
 
         response_json = response.json()
diff --git a/canvasapi/authentication_event.py b/canvasapi/authentication_event.py
new file mode 100644
index 0000000..4ba6286
--- /dev/null
+++ b/canvasapi/authentication_event.py
@@ -0,0 +1,11 @@
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+from six import python_2_unicode_compatible
+
+from canvasapi.canvas_object import CanvasObject
+
+
+@python_2_unicode_compatible
+class AuthenticationEvent(CanvasObject):
+    def __str__(self):
+        return "{} {} ({})".format(self.created_at, self.event_type, self.pseudonym_id)
diff --git a/canvasapi/canvas.py b/canvasapi/canvas.py
index ec235a7..1ce81bd 100644
--- a/canvasapi/canvas.py
+++ b/canvasapi/canvas.py
@@ -14,6 +14,7 @@ from canvasapi.paginated_list import PaginatedList
 from canvasapi.requester import Requester
 from canvasapi.section import Section
 from canvasapi.user import User
+from canvasapi.comm_message import CommMessage
 from canvasapi.util import combine_kwargs, get_institution_url, obj_or_id
 
 
@@ -58,6 +59,11 @@ class Canvas(object):
                 UserWarning,
             )
 
+        # Ensure that the user-supplied access token contains no leading or
+        # trailing spaces that may cause issues when communicating with
+        # the API.
+        access_token = access_token.strip()
+
         base_url = new_url + "/api/v1/"
 
         self.__requester = Requester(base_url, access_token)
@@ -491,6 +497,19 @@ class Canvas(object):
             _kwargs=combine_kwargs(**kwargs),
         )
 
+    def get_brand_variables(self):
+        """
+        Get account brand variables
+
+        :calls: `GET /api/v1/brand_variables \
+        <https://canvas.instructure.com/doc/api/brand_configs.html>`_
+
+        :returns: JSON with brand variables for the account.
+        :rtype: dict
+        """
+        response = self.__requester.request("GET", "brand_variables")
+        return response.json()
+
     def get_calendar_event(self, calendar_event):
         """
         Return single Calendar Event by id
@@ -534,6 +553,32 @@ class Canvas(object):
             _kwargs=combine_kwargs(**kwargs),
         )
 
+    def get_comm_messages(self, user, **kwargs):
+        """
+        Retrieve a paginated list of messages sent to a user.
+
+        :calls: `GET /api/v1/comm_messages \
+        <https://canvas.instructure.com/doc/api/comm_messages.html#method.comm_messages_api.index>`_
+
+        :param user: The object or ID of the user.
+        :type user: :class: `canvasapi.user.User` or int
+
+        :returns: Paginated list containing messages sent to user
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+            :class:`canvasapi.comm_message.CommMessage`
+
+        """
+
+        kwargs["user_id"] = obj_or_id(user, "user", (User,))
+
+        return PaginatedList(
+            CommMessage,
+            self.__requester,
+            "GET",
+            "comm_messages",
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
     def get_conversation(self, conversation, **kwargs):
         """
         Return single Conversation
@@ -1080,7 +1125,7 @@ class Canvas(object):
         response = self.__requester.request("GET", "users/self/upcoming_events")
         return response.json()
 
-    def get_user(self, user, id_type=None):
+    def get_user(self, user, id_type=None, **kwargs):
         """
         Retrieve a user by their ID. `id_type` denotes which endpoint to try as there are
         several different IDs that can pull the same user record from Canvas.
@@ -1107,7 +1152,9 @@ class Canvas(object):
             user_id = obj_or_id(user, "user", (User,))
             uri = "users/{}".format(user_id)
 
-        response = self.__requester.request("GET", uri)
+        response = self.__requester.request(
+            "GET", uri, _kwargs=combine_kwargs(**kwargs)
+        )
         return User(self.__requester, response.json())
 
     def get_user_participants(self, appointment_group, **kwargs):
diff --git a/canvasapi/canvas_object.py b/canvasapi/canvas_object.py
index 184c26f..96ef03b 100644
--- a/canvasapi/canvas_object.py
+++ b/canvasapi/canvas_object.py
@@ -1,8 +1,10 @@
 from __future__ import absolute_import, division, print_function, unicode_literals
+
 from datetime import datetime
 import json
 import pytz
 import re
+import warnings
 
 from six import text_type
 
@@ -17,6 +19,14 @@ class CanvasObject(object):
     to dynamically construct this object's attributes with a JSON object.
     """
 
+    def __getattribute__(self, name):
+        if name == "attributes":
+            warnings.warn(
+                "CanvasObject.attributes is deprecated and will be removed in a future version.",
+                DeprecationWarning,
+            )
+        return super(CanvasObject, self).__getattribute__(name)
+
     def __init__(self, requester, attributes):
         """
         :param requester: The requester to pass HTTP requests through.
@@ -77,5 +87,16 @@ class CanvasObject(object):
         """
         Return the original JSON response from the API that was used to
         construct the object.
+
+        .. warning::
+            .. deprecated:: 0.15.0
+                To view the original attributes sent by Canvas, enable logs from the
+                requests library.
         """
+        warnings.warn(
+            "`CanvasObject.to_json()` is deprecated and will be removed in a future version. "
+            "To view the original attributes sent by Canvas, enable logs from the "
+            "requests library",
+            DeprecationWarning,
+        )
         return json.dumps(self.attributes)
diff --git a/canvasapi/comm_message.py b/canvasapi/comm_message.py
new file mode 100644
index 0000000..ad6de8c
--- /dev/null
+++ b/canvasapi/comm_message.py
@@ -0,0 +1,11 @@
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+from six import python_2_unicode_compatible
+
+from canvasapi.canvas_object import CanvasObject
+
+
+@python_2_unicode_compatible
+class CommMessage(CanvasObject):
+    def __str__(self):
+        return "{} ({})".format(self.subject, self.id)
diff --git a/canvasapi/course.py b/canvasapi/course.py
index e42cd7a..08d723b 100644
--- a/canvasapi/course.py
+++ b/canvasapi/course.py
@@ -4,16 +4,24 @@ import warnings
 
 from six import python_2_unicode_compatible, text_type, string_types
 
+from canvasapi.assignment import Assignment, AssignmentGroup
 from canvasapi.blueprint import BlueprintSubscription
 from canvasapi.canvas_object import CanvasObject
 from canvasapi.collaboration import Collaboration
 from canvasapi.course_epub_export import CourseEpubExport
 from canvasapi.discussion_topic import DiscussionTopic
+from canvasapi.gradebook_history import (
+    Day,
+    Grader,
+    SubmissionVersion,
+    SubmissionHistory,
+)
 from canvasapi.grading_standard import GradingStandard
 from canvasapi.grading_period import GradingPeriod
 from canvasapi.exceptions import RequiredFieldMissing
 from canvasapi.feature import Feature, FeatureFlag
 from canvasapi.folder import Folder
+from canvasapi.license import License
 from canvasapi.outcome_import import OutcomeImport
 from canvasapi.page import Page
 from canvasapi.paginated_list import PaginatedList
@@ -23,6 +31,7 @@ from canvasapi.tab import Tab
 from canvasapi.rubric import RubricAssociation, Rubric
 from canvasapi.submission import GroupedSubmission, Submission
 from canvasapi.upload import Uploader
+from canvasapi.usage_rights import UsageRights
 from canvasapi.util import (
     combine_kwargs,
     is_multivalued,
@@ -467,6 +476,29 @@ class Course(CanvasObject):
 
         return rubric_dict
 
+    def create_rubric_association(self, **kwargs):
+        """
+        Create a new RubricAssociation.
+
+        :calls: `POST /api/v1/courses/:course_id/rubric_associations \
+        <https://canvas.instructure.com/doc/api/rubrics.html#method.rubric_associations.create>`_
+
+        :returns: Returns a RubricAssociation.
+        :rtype: :class:`canvasapi.rubric.RubricAssociation`
+        """
+        from canvasapi.rubric import RubricAssociation
+
+        response = self._requester.request(
+            "POST",
+            "courses/{}/rubric_associations".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        quiz_json = response.json()
+        quiz_json.update({"course_id": self.id})
+
+        return RubricAssociation(self._requester, quiz_json)
+
     def delete(self):
         """
         Permanently delete this course.
@@ -708,6 +740,34 @@ class Course(CanvasObject):
             _kwargs=combine_kwargs(**kwargs),
         )
 
+    def get_assignments_for_group(self, assignment_group, **kwargs):
+        """
+        Returns a paginated list of assignments for the given assignment group
+
+        :calls: `GET /api/v1/courses/:course_id/assignment_groups/:assignment_group_id/assignments\
+        <https://canvas.instructure.com/doc/api/assignments.html#method.assignments_api.index>`_
+
+        :param assignment_group: The object or id of the assignment group
+        :type assignment_group: :class: `canvasapi.assignment.AssignmentGroup` or int
+
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+            :class:`canvasapi.assignment.Assignment`
+        """
+
+        assignment_group_id = obj_or_id(
+            assignment_group, "assignment_group", (AssignmentGroup,)
+        )
+
+        return PaginatedList(
+            Assignment,
+            self._requester,
+            "GET",
+            "courses/{}/assignment_groups/{}/assignments".format(
+                self.id, assignment_group_id
+            ),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
     def get_blueprint(self, template="default", **kwargs):
         """
         Return the blueprint of a given ID.
@@ -1203,6 +1263,51 @@ class Course(CanvasObject):
         )
         return response.json()
 
+    def get_gradebook_history_dates(self, **kwargs):
+        """
+        Returns a map of dates to grader/assignment groups
+
+        :calls: `GET /api/v1/courses/:course_id/gradebook_history/days\
+        <https://canvas.instructure.com/doc/api/gradebook_history.html#method.gradebook_history_api.days>`_
+
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+            :class:`canvasapi.grading_history.Day`
+        """
+
+        return PaginatedList(
+            Day,
+            self._requester,
+            "GET",
+            "courses/{}/gradebook_history/days".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+    def get_gradebook_history_details(self, date, **kwargs):
+        """
+        Returns the graders who worked on this day, along with the
+        assignments they worked on. More details can be obtained by
+        selecting a grader and assignment and calling the 'submissions'
+        api endpoint for a given date.
+
+        :calls: `GET /api/v1/courses/:course_id/gradebook_history/:date\
+        <https://canvas.instructure.com/doc/api/gradebook_history.html#method.\
+        gradebook_history_api.day_details>`_
+
+        :param date: The date for which you would like to see detailed information.
+        :type date: int
+
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+            :class:`canvasapi.gradebook_history.Grader`
+        """
+
+        return PaginatedList(
+            Grader,
+            self._requester,
+            "GET",
+            "courses/{}/gradebook_history/{}".format(self.id, date),
+            kwargs=combine_kwargs(**kwargs),
+        )
+
     def get_grading_period(self, grading_period, **kwargs):
         """
         Return a single grading period for the associated course and id.
@@ -1305,6 +1410,26 @@ class Course(CanvasObject):
             _kwargs=combine_kwargs(**kwargs),
         )
 
+    def get_licenses(self, **kwargs):
+        """
+        Returns a paginated list of the licenses that can be applied to the
+        files under the course scope
+
+        :calls: `GET /api/v1/course/:course_id/content_licenses \
+        <https://canvas.instructure.com/doc/api/files.html#method.usage_rights.licenses>`_
+
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+            :class:`canvasapi.license.License`
+        """
+
+        return PaginatedList(
+            License,
+            self._requester,
+            "GET",
+            "courses/{}/content_licenses".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
     def get_migration_systems(self, **kwargs):
         """
         Return a list of migration systems.
@@ -1773,6 +1898,36 @@ class Course(CanvasObject):
 
         return assignment.get_submission(user, **kwargs)
 
+    def get_submission_history(self, date, grader_id, assignment_id, **kwargs):
+        """
+        Gives a nested list of submission versions.
+
+        :calls: `GET /api/v1/courses/:course_id/gradebook_history/:date/graders\
+        /:grader_id/assignments/:assignment_id/submissions\
+        <https://canvas.instructure.com/doc/api/gradebook_history.html#method.\
+        gradebook_history_api.submissions>`_
+
+        :param date: The date for which you would like to see submissions
+        :type grader_id: str
+        :param grader_id: The ID of the grader for which you want to see submissions.
+        :type grader_id: int
+        :param assignment_id: The ID of the assignment for which you want to see submissions
+        :type assignment_id: int
+
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+            :class:`canvasapi.gradebook_history.SubmissionHistory`
+        """
+
+        return PaginatedList(
+            SubmissionHistory,
+            self._requester,
+            "GET",
+            "courses/{}/gradebook_history/{}/graders/{}/assignments/{}/submissions".format(
+                self.id, date, grader_id, assignment_id
+            ),
+            kwargs=combine_kwargs(**kwargs),
+        )
+
     def get_tabs(self, **kwargs):
         """
         List available tabs for a course.
@@ -1793,7 +1948,30 @@ class Course(CanvasObject):
             _kwargs=combine_kwargs(**kwargs),
         )
 
-    def get_user(self, user, user_id_type=None):
+    def get_uncollated_submissions(self, **kwargs):
+        """
+        Gives a paginated, uncollated list of submission versions for all matching
+        submissions in the context. This SubmissionVersion objects will not include
+        the new_grade or previous_grade keys, only the grade; same for graded_at
+        and grader.
+
+        :calls: `GET /api/v1/courses/:course_id/gradebook_history/feed\
+        <https://canvas.instructure.com/doc/api/gradebook_history.html#method\
+        .gradebook_history_api.feed>`_
+
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+            :class:`canvasapi.gradebook_history.SubmissionVersion`
+        """
+
+        return PaginatedList(
+            SubmissionVersion,
+            self._requester,
+            "GET",
+            "courses/{}/gradebook_history/feed".format(self.id),
+            kwargs=combine_kwargs(**kwargs),
+        )
+
+    def get_user(self, user, user_id_type=None, **kwargs):
         """
         Retrieve a user by their ID. `user_id_type` denotes which endpoint to try as there are
         several different ids that can pull the same user record from Canvas.
@@ -1816,7 +1994,7 @@ class Course(CanvasObject):
             user_id = obj_or_id(user, "user", (User,))
             uri = "courses/{}/users/{}".format(self.id, user_id)
 
-        response = self._requester.request("GET", uri)
+        response = self._requester.request("GET", uri, _kwargs=combine_kwargs(**kwargs))
         return User(self._requester, response.json())
 
     def get_user_in_a_course_level_assignment_data(self, user):
@@ -2337,6 +2515,24 @@ class Course(CanvasObject):
         )
         return response.json().get("html", "")
 
+    def remove_usage_rights(self, **kwargs):
+        """
+        Removes the usage rights for specified files that are under the current course scope
+
+        :calls: `DELETE /api/v1/courses/:course_id/usage_rights \
+        <https://canvas.instructure.com/doc/api/files.html#method.usage_rights.remove_usage_rights>`_
+
+        :rtype: dict
+        """
+
+        response = self._requester.request(
+            "DELETE",
+            "courses/{}/usage_rights".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        return response.json()
+
     def reorder_pinned_topics(self, order):
         """
         Puts the pinned discussion topics in the specified order.
@@ -2435,6 +2631,24 @@ class Course(CanvasObject):
             QuizExtension(self._requester, extension) for extension in extension_list
         ]
 
+    def set_usage_rights(self, **kwargs):
+        """
+        Changes the usage rights for specified files that are under the current course scope
+
+        :calls: `PUT /api/v1/courses/:course_id/usage_rights \
+        <https://canvas.instructure.com/doc/api/files.html#method.usage_rights.set_usage_rights>`_
+
+        :rtype: :class:`canvasapi.usage_rights.UsageRights`
+        """
+
+        response = self._requester.request(
+            "PUT",
+            "courses/{}/usage_rights".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        return UsageRights(self._requester, response.json())
+
     def show_front_page(self):
         """
         Retrieve the content of the front page.
diff --git a/canvasapi/gradebook_history.py b/canvasapi/gradebook_history.py
new file mode 100644
index 0000000..34cb528
--- /dev/null
+++ b/canvasapi/gradebook_history.py
@@ -0,0 +1,29 @@
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+from six import python_2_unicode_compatible
+
+from canvasapi.canvas_object import CanvasObject
+
+
+@python_2_unicode_compatible
+class Day(CanvasObject):
+    def __str__(self):
+        return "{}".format(self.date)
+
+
+@python_2_unicode_compatible
+class Grader(CanvasObject):
+    def __str__(self):
+        return "{}".format(self.id)
+
+
+@python_2_unicode_compatible
+class SubmissionHistory(CanvasObject):
+    def __str__(self):
+        return "{}".format(self.submission_id)
+
+
+@python_2_unicode_compatible
+class SubmissionVersion(CanvasObject):
+    def __str__(self):
+        return "{} {}".format(self.assignment_id, self.id)
diff --git a/canvasapi/group.py b/canvasapi/group.py
index 1804509..758a8a2 100644
--- a/canvasapi/group.py
+++ b/canvasapi/group.py
@@ -9,8 +9,10 @@ from canvasapi.collaboration import Collaboration
 from canvasapi.discussion_topic import DiscussionTopic
 from canvasapi.folder import Folder
 from canvasapi.exceptions import RequiredFieldMissing
+from canvasapi.license import License
 from canvasapi.paginated_list import PaginatedList
 from canvasapi.tab import Tab
+from canvasapi.usage_rights import UsageRights
 from canvasapi.util import combine_kwargs, is_multivalued, obj_or_id
 
 
@@ -563,6 +565,26 @@ class Group(CanvasObject):
         )
         return response.json()
 
+    def get_licenses(self, **kwargs):
+        """
+        Returns a paginated list of the licenses that can be applied to the
+        files under the group scope
+
+        :calls: `GET /api/v1/groups/:group_id/content_licenses \
+        <https://canvas.instructure.com/doc/api/files.html#method.usage_rights.licenses>`_
+
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+            :class:`canvasapi.license.License`
+        """
+
+        return PaginatedList(
+            License,
+            self._requester,
+            "GET",
+            "groups/{}/content_licenses".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
     def get_membership(self, user, membership_type):
         """
         List users in a group.
@@ -879,6 +901,23 @@ class Group(CanvasObject):
         )
         return response.json().get("html", "")
 
+    def remove_usage_rights(self, **kwargs):
+        """
+        Removes the usage rights for specified files that are under the current group scope
+
+        :calls: `DELETE /api/v1/groups/:group_id/usage_rights \
+        <https://canvas.instructure.com/doc/api/files.html#method.usage_rights.remove_usage_rights>`_
+
+        :rtype: dict
+        """
+        response = self._requester.request(
+            "DELETE",
+            "groups/{}/usage_rights".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        return response.json()
+
     def remove_user(self, user):
         """
         Leave a group if allowed.
@@ -929,6 +968,24 @@ class Group(CanvasObject):
 
         return response.json().get("reorder")
 
+    def set_usage_rights(self, **kwargs):
+        """
+        Changes the usage rights for specified files that are under the current group scope
+
+        :calls: `PUT /api/v1/groups/:group_id/usage_rights \
+        <https://canvas.instructure.com/doc/api/files.html#method.usage_rights.set_usage_rights>`_
+
+        :rtype: :class:`canvasapi.usage_rights.UsageRights`
+        """
+
+        response = self._requester.request(
+            "PUT",
+            "groups/{}/usage_rights".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        return UsageRights(self._requester, response.json())
+
     def show_front_page(self):
         """
         Retrieve the content of the front page.
diff --git a/canvasapi/license.py b/canvasapi/license.py
new file mode 100644
index 0000000..2400e29
--- /dev/null
+++ b/canvasapi/license.py
@@ -0,0 +1,11 @@
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+from six import python_2_unicode_compatible
+
+from canvasapi.canvas_object import CanvasObject
+
+
+@python_2_unicode_compatible
+class License(CanvasObject):
+    def __str__(self):
+        return "{} {}".format(self.name, self.id)
diff --git a/canvasapi/login.py b/canvasapi/login.py
index e43659e..d937e3f 100644
--- a/canvasapi/login.py
+++ b/canvasapi/login.py
@@ -3,6 +3,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera
 from six import python_2_unicode_compatible
 
 from canvasapi.canvas_object import CanvasObject
+from canvasapi.paginated_list import PaginatedList
 from canvasapi.util import combine_kwargs
 
 
@@ -40,3 +41,23 @@ class Login(CanvasObject):
             _kwargs=combine_kwargs(**kwargs),
         )
         return Login(self._requester, response.json())
+
+    def get_authentication_events(self, **kwargs):
+        """
+        List authentication events for a given login.
+
+        :calls: `GET /api/v1/audit/authentication/logins/:login_id \
+        <https://canvas.instructure.com/doc/api/authentications_log.html#method.authentication_audit_api.for_login>`_
+
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+                :class:`canvasapi.authentication_event.AuthenticationEvent`
+        """
+        from canvasapi.authentication_event import AuthenticationEvent
+
+        return PaginatedList(
+            AuthenticationEvent,
+            self._requester,
+            "GET",
+            "audit/authentication/logins/{}".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
diff --git a/canvasapi/paginated_list.py b/canvasapi/paginated_list.py
index 21cceaa..90ceade 100644
--- a/canvasapi/paginated_list.py
+++ b/canvasapi/paginated_list.py
@@ -11,6 +11,8 @@ class PaginatedList(object):
     def __getitem__(self, index):
         assert isinstance(index, (int, slice))
         if isinstance(index, int):
+            if index < 0:
+                raise IndexError("Cannot negative index a PaginatedList")
             self._get_up_to_index(index)
             return self._elements[index]
         else:
@@ -105,6 +107,9 @@ class PaginatedList(object):
             self._stop = the_slice.stop
             self._step = the_slice.step or 1
 
+            if self._start < 0 or self._stop < 0:
+                raise IndexError("Cannot negative index a PaginatedList slice")
+
         def __iter__(self):
             index = self._start
             while not self._finished(index):
diff --git a/canvasapi/quiz.py b/canvasapi/quiz.py
index 3326286..4e149c0 100644
--- a/canvasapi/quiz.py
+++ b/canvasapi/quiz.py
@@ -16,6 +16,44 @@ class Quiz(CanvasObject):
     def __str__(self):
         return "{} ({})".format(self.title, self.id)
 
+    def broadcast_message(self, conversations, **kwargs):
+        """
+        Send a message to unsubmitted or submitted users for the quiz.
+
+        :calls: `POST /api/v1/courses/:course_id/quizzes/:id/submission_users/message \
+        <https://canvas.instructure.com/doc/api/quiz_submission_user_list.html#method.quizzes/quiz_submission_users.message>`_
+
+        :param conversations: A dictionary representing a Conversation.
+            Requires `'body'`, `'recipients'`, and `'subject'` keys.
+        :type conversations: dict
+
+        :returns: True if the message was created, False otherwize
+        :rtype: bool
+        """
+
+        required_key_list = ["body", "recipients", "subject"]
+        required_keys_present = all((x in conversations for x in required_key_list))
+
+        if isinstance(conversations, dict) and required_keys_present:
+            kwargs["conversations"] = conversations
+        else:
+            raise RequiredFieldMissing(
+                (
+                    "conversations must be a dictionary with keys "
+                    "'body', 'recipients', and 'subject'."
+                )
+            )
+
+        response = self._requester.request(
+            "POST",
+            "courses/{}/quizzes/{}/submission_users/message".format(
+                self.course_id, self.id
+            ),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        return response.status_code == 201
+
     def create_question(self, **kwargs):
         """
         Create a new quiz question for this quiz.
@@ -31,6 +69,7 @@ class Quiz(CanvasObject):
             "courses/{}/quizzes/{}/questions".format(self.course_id, self.id),
             _kwargs=combine_kwargs(**kwargs),
         )
+
         response_json = response.json()
         response_json.update({"course_id": self.course_id})
 
@@ -82,6 +121,39 @@ class Quiz(CanvasObject):
 
         return QuizGroup(self._requester, response_json.get("quiz_groups")[0])
 
+    def create_report(self, report_type, **kwargs):
+        """
+        Create and return a new report for this quiz. If a previously generated report
+        matches the arguments and is still current (i.e. there have been no new submissions),
+        it will be returned.
+
+        :calls: `POST /api/v1/courses/:course_id/quizzes/:quiz_id/reports \
+        <https://canvas.instructure.com/doc/api/quiz_reports.html#method.quizzes/quiz_reports.create>`_
+
+        :param report_type: The type of report, either student_analysis or item_analysis
+        :type report_type: str
+
+        :returns: `QuizReport` object
+        :rtype: :class:`canvasapi.quiz.QuizReport`
+        """
+        if report_type not in ["student_analysis", "item_analysis"]:
+            raise ValueError(
+                "Param `report_type` must be a either 'student_analysis' or 'item_analysis'"
+            )
+
+        kwargs["report_type"] = report_type
+
+        response = self._requester.request(
+            "POST",
+            "courses/{}/quizzes/{}/reports".format(self.course_id, self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        response_json = response.json()
+        response_json.update({"course_id": self.course_id})
+
+        return QuizReport(self._requester, response_json)
+
     def create_submission(self, **kwargs):
         """
         Start taking a Quiz by creating a QuizSubmission can be used to answer
@@ -142,6 +214,24 @@ class Quiz(CanvasObject):
 
         return Quiz(self._requester, quiz_json)
 
+    def get_all_quiz_reports(self, **kwargs):
+        """
+        Get a list of all quiz reports for this quiz
+
+        :calls: `GET /api/v1/courses/:course_id/quizzes/:quiz_id/reports \
+        <https://canvas.instructure.com/doc/api/quiz_reports.html#method.quizzes/quiz_reports.index>`_
+
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+            :class:`canvasapi.quiz.QuizReport`
+        """
+        return PaginatedList(
+            QuizReport,
+            self._requester,
+            "GET",
+            "courses/{}/quizzes/{}/reports".format(self.course_id, self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
     def get_all_quiz_submissions(self, **kwargs):
         """
         Get a list of all submissions for this quiz.
@@ -232,6 +322,32 @@ class Quiz(CanvasObject):
 
         return QuizGroup(self._requester, response_json)
 
+    def get_quiz_report(self, id, **kwargs):
+        """
+        Returns the data for a single quiz report.
+
+        :calls: `GET /api/v1/courses/:course_id/quizzes/:quiz_id/reports/:id \
+        <https://canvas.instructure.com/doc/api/quiz_reports.html#method.quizzes/quiz_reports.show>`_
+
+        :param id: The ID of the quiz report you want to retrieve, or the report object
+        :type id: int or :class:`canvasapi.quiz.QuizReport`
+
+        :returns: `QuizReport` object
+        :rtype: :class:`canvasapi.quiz.QuizReport`
+        """
+        id = obj_or_id(id, "id", (QuizReport,))
+
+        response = self._requester.request(
+            "GET",
+            "courses/{}/quizzes/{}/reports/{}".format(self.course_id, self.id, id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        response_json = response.json()
+        response_json.update({"course_id": self.course_id})
+
+        return QuizReport(self._requester, response_json)
+
     def get_quiz_submission(self, quiz_submission, **kwargs):
         """
         Get a single quiz submission.
@@ -276,6 +392,7 @@ class Quiz(CanvasObject):
             self._requester,
             "GET",
             "courses/{}/quizzes/{}/submissions".format(self.course_id, self.id),
+            {"course_id": self.course_id},
             _root="quiz_submissions",
             _kwargs=combine_kwargs(**kwargs),
         )
@@ -423,6 +540,27 @@ class QuizSubmission(CanvasObject):
         response_json = response.json()["quiz_submissions"][0]
         return QuizSubmission(self._requester, response_json)
 
+    def get_submission_events(self, **kwargs):
+        """
+        Retrieve the set of events captured during a specific submission attempt.
+
+        :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
+        """
+        response = self._requester.request(
+            "GET",
+            "courses/{}/quizzes/{}/submissions/{}/events".format(
+                self.course_id, self.quiz_id, self.id
+            ),
+            _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):
         """
         Get a list of all the question records for this quiz submission.
@@ -466,6 +604,37 @@ class QuizSubmission(CanvasObject):
 
         return response.json()
 
+    def submit_events(self, quiz_submission_events, **kwargs):
+        """
+        Store a set of events which were captured during a quiz taking session.
+
+        :calls: `POST /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.create>`_
+
+        :param quiz_submission_events: The submission events to be recorded.
+        :type quiz_submission_events: list
+
+        :returns: True if the submission was successful, false otherwise.
+        :rtype: bool
+        """
+        if isinstance(quiz_submission_events, list) and isinstance(
+            quiz_submission_events[0], QuizSubmissionEvent
+        ):
+            kwargs["quiz_submission_events"] = quiz_submission_events
+        else:
+            raise RequiredFieldMissing(
+                "Required parameter quiz_submission_events missing."
+            )
+
+        response = self._requester.request(
+            "POST",
+            "courses/{}/quizzes/{}/submissions/{}/events".format(
+                self.course_id, self.quiz_id, self.id
+            ),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+        return response.status_code == 204
+
     def update_score_and_comments(self, **kwargs):
         """
         Update the amount of points a student has scored for questions they've answered, provide
@@ -486,6 +655,7 @@ class QuizSubmission(CanvasObject):
             _kwargs=combine_kwargs(**kwargs),
         )
         response_json = response.json()["quiz_submissions"][0]
+        response_json.update({"course_id": self.course_id})
 
         return QuizSubmission(self._requester, response_json)
 
@@ -544,6 +714,40 @@ class QuizQuestion(CanvasObject):
         return self
 
 
+@python_2_unicode_compatible
+class QuizReport(CanvasObject):
+    def __str__(self):
+        return "{} ({})".format(self.report_type, self.id)
+
+    def abort_or_delete(self, **kwargs):
+        """
+        This API allows you to cancel a previous request you issued for a report to be generated.
+        Or in the case of an already generated report, you'd like to remove it, perhaps to generate
+        it another time with an updated version that provides new features.
+
+        :calls: `DELETE /api/v1/courses/:course_id/quizzes/:quiz_id/reports/:id \
+        <https://canvas.instructure.com/doc/api/quiz_reports.html#method.quizzes/quiz_reports.abort>`_
+
+        :returns: True if attempt was successful; False otherwise
+        :rtype: bool
+        """
+        response = self._requester.request(
+            "DELETE",
+            "courses/{}/quizzes/{}/reports/{}".format(
+                self.course_id, self.quiz_id, self.id
+            ),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        return response.status_code == 204
+
+
+@python_2_unicode_compatible
+class QuizSubmissionEvent(CanvasObject):
+    def __str__(self):
+        return "{}".format(self.event_type)
+
+
 @python_2_unicode_compatible
 class QuizSubmissionQuestion(CanvasObject):
     def __str__(self):
diff --git a/canvasapi/rubric.py b/canvasapi/rubric.py
index 4b58436..b323483 100644
--- a/canvasapi/rubric.py
+++ b/canvasapi/rubric.py
@@ -3,6 +3,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera
 from six import python_2_unicode_compatible
 
 from canvasapi.canvas_object import CanvasObject
+from canvasapi.util import combine_kwargs
 
 
 @python_2_unicode_compatible
@@ -15,3 +16,47 @@ class Rubric(CanvasObject):
 class RubricAssociation(CanvasObject):
     def __str__(self):
         return "{}, {}".format(self.id, self.association_type)
+
+    def delete(self, **kwargs):
+        """
+        Delete a RubricAssociation.
+
+        :calls: `DELETE /api/v1/courses/:course_id/rubric_associations/:id \
+        <https://canvas.instructure.com/doc/api/rubrics.html#method.rubric_associations.destroy>`_
+
+        :rtype: :class:`canvasapi.rubric.RubricAssociation`
+        """
+        from canvasapi.rubric import RubricAssociation
+
+        response = self._requester.request(
+            "DELETE",
+            "courses/{}/rubric_associations/{}".format(self.course_id, self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        return RubricAssociation(self._requester, response.json())
+
+    def update(self, **kwargs):
+        """
+        Update a RubricAssociation.
+
+        :calls: `PUT /api/v1/courses/:course_id/rubric_associations/:id \
+        <https://canvas.instructure.com/doc/api/rubrics.html#method.rubric_associations.update>`_
+
+        :returns: Returns a RubricAssociation.
+        :rtype: :class:`canvasapi.rubric.RubricAssociation`
+        """
+        from canvasapi.rubric import RubricAssociation
+
+        response = self._requester.request(
+            "PUT",
+            "courses/{}/rubric_associations/{}".format(self.course_id, self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        response_json = response.json()
+        response_json.update({"course_id": self.course_id})
+        if "association_type" in response_json:
+            super(RubricAssociation, self).set_attributes(response_json)
+
+        return self
diff --git a/canvasapi/submission.py b/canvasapi/submission.py
index 588a24a..15df324 100644
--- a/canvasapi/submission.py
+++ b/canvasapi/submission.py
@@ -179,17 +179,16 @@ class Submission(CanvasObject):
 @python_2_unicode_compatible
 class GroupedSubmission(CanvasObject):
     def __init__(self, requester, attributes):
+        super(GroupedSubmission, self).__init__(requester, attributes)
+
         try:
             self.submissions = [
                 Submission(requester, submission)
                 for submission in attributes["submissions"]
             ]
-            del attributes["submissions"]
         except KeyError:
             self.submissions = list()
 
-        super(GroupedSubmission, self).__init__(requester, attributes)
-
     def __str__(self):
         return "{} submission(s) for User #{}".format(
             len(self.submissions), self.user_id
diff --git a/canvasapi/usage_rights.py b/canvasapi/usage_rights.py
new file mode 100644
index 0000000..1de8adb
--- /dev/null
+++ b/canvasapi/usage_rights.py
@@ -0,0 +1,11 @@
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+from six import python_2_unicode_compatible
+
+from canvasapi.canvas_object import CanvasObject
+
+
+@python_2_unicode_compatible
+class UsageRights(CanvasObject):
+    def __str__(self):
+        return "{} {}".format(self.use_justification, self.license)
diff --git a/canvasapi/user.py b/canvasapi/user.py
index d560c72..116727c 100644
--- a/canvasapi/user.py
+++ b/canvasapi/user.py
@@ -9,7 +9,9 @@ from canvasapi.communication_channel import CommunicationChannel
 from canvasapi.feature import Feature, FeatureFlag
 from canvasapi.folder import Folder
 from canvasapi.paginated_list import PaginatedList
+from canvasapi.license import License
 from canvasapi.upload import Uploader
+from canvasapi.usage_rights import UsageRights
 from canvasapi.util import combine_kwargs, obj_or_id, obj_or_str
 
 
@@ -185,6 +187,26 @@ class User(CanvasObject):
             _kwargs=combine_kwargs(**kwargs),
         )
 
+    def get_authentication_events(self, **kwargs):
+        """
+        List authentication events for a given user.
+
+        :calls: `GET /api/v1/audit/authentication/users/:user_id \
+        <https://canvas.instructure.com/doc/api/authentications_log.html#method.authentication_audit_api.for_user>`_
+
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+                :class:`canvasapi.authentication_event.AuthenticationEvent`
+        """
+        from canvasapi.authentication_event import AuthenticationEvent
+
+        return PaginatedList(
+            AuthenticationEvent,
+            self._requester,
+            "GET",
+            "audit/authentication/users/{}".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
     def get_avatars(self):
         """
         Retrieve the possible user avatar options that can be set with the user update endpoint.
@@ -565,6 +587,26 @@ class User(CanvasObject):
             _kwargs=combine_kwargs(**kwargs),
         )
 
+    def get_licenses(self, **kwargs):
+        """
+        Returns a paginated list of the licenses that can be applied to the
+        files under the user scope
+
+        :calls: `GET /api/v1/users/:user_id/content_licenses \
+        <https://canvas.instructure.com/doc/api/files.html#method.usage_rights.licenses>`_
+
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+            :class:`canvasapi.license.License`
+        """
+
+        return PaginatedList(
+            License,
+            self._requester,
+            "GET",
+            "users/{}/content_licenses".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
     def get_migration_systems(self, **kwargs):
         """
         Return a list of migration systems.
@@ -870,6 +912,42 @@ class User(CanvasObject):
         )
         return User(self._requester, response.json())
 
+    def remove_usage_rights(self, **kwargs):
+        """
+        Changes the usage rights for specified files that are under the user scope
+
+        :calls: `DELETE /api/v1/users/:user_id/usage_rights \
+        <https://canvas.instructure.com/doc/api/files.html#method.usage_rights.remove_usage_rights>`_
+
+        :rtype: dict
+        """
+
+        response = self._requester.request(
+            "DELETE",
+            "users/{}/usage_rights".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        return response.json()
+
+    def set_usage_rights(self, **kwargs):
+        """
+        Changes the usage rights for specified files that are under the user scope
+
+        :calls: `PUT /api/v1/users/:user_id/usage_rights \
+        <https://canvas.instructure.com/doc/api/files.html#method.usage_rights.set_usage_rights>`_
+
+        :rtype: :class:`canvasapi.usage_rights.UsageRights`
+        """
+
+        response = self._requester.request(
+            "PUT",
+            "users/{}/usage_rights".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        return UsageRights(self._requester, response.json())
+
     def show_observee(self, observee_id):
         """
         Gets information about an observed user.
diff --git a/canvasapi/util.py b/canvasapi/util.py
index 976ecdd..5e82d57 100644
--- a/canvasapi/util.py
+++ b/canvasapi/util.py
@@ -1,7 +1,6 @@
 from __future__ import absolute_import, division, print_function, unicode_literals
 
 import os
-from collections import Iterable
 
 from six import binary_type, string_types, text_type
 
@@ -25,7 +24,11 @@ def is_multivalued(value):
         return False
 
     # general rule: multivalued if iterable
-    return isinstance(value, Iterable)
+    try:
+        iter(value)
+        return True
+    except TypeError:
+        return False
 
 
 def combine_kwargs(**kwargs):
@@ -244,12 +247,7 @@ def clean_headers(headers):
 
     authorization_header = headers.get("Authorization")
     if authorization_header:
-        # Grab the actual token (not the "Bearer" prefix)
-        _, token = authorization_header.split(" ")
-
-        # Trim all but the last four characters
-        sanitized = "****" + token[-4:]
-
+        sanitized = "****" + authorization_header[-4:]
         cleaned_headers["Authorization"] = sanitized
 
     return cleaned_headers
diff --git a/docs/assignment-ref.rst b/docs/assignment-ref.rst
index e0612a2..dd1e5ea 100644
--- a/docs/assignment-ref.rst
+++ b/docs/assignment-ref.rst
@@ -5,6 +5,11 @@ Assignment
 .. autoclass:: canvasapi.assignment.Assignment
     :members:
 
+===================
+AssignmentExtension
+===================
+.. autoclass:: canvasapi.assignment.AssignmentExtension
+    :members:
 
 ===============
 AssignmentGroup
diff --git a/docs/authentication-event-ref.rst b/docs/authentication-event-ref.rst
new file mode 100644
index 0000000..6b28072
--- /dev/null
+++ b/docs/authentication-event-ref.rst
@@ -0,0 +1,6 @@
+===================
+AuthenticationEvent
+===================
+
+.. autoclass:: canvasapi.authentication_event.AuthenticationEvent
+    :members:
diff --git a/docs/class-reference.rst b/docs/class-reference.rst
index 6ef8966..4da3343 100644
--- a/docs/class-reference.rst
+++ b/docs/class-reference.rst
@@ -7,12 +7,14 @@ Class Reference
     account-ref
     appointment-group-ref
     assignment-ref
+    authentication-event-ref
     authentication-provider-ref
     avatar-ref
     blueprint-ref
     bookmark-ref
     calendar-event-ref
     collaboration-ref
+    comm-message-ref
     communication-channel-ref
     content-export-ref
     content-migration-ref
@@ -32,6 +34,7 @@ Class Reference
     grading-period-ref
     group-ref
     login-ref
+    license-ref
     module-ref
     outcome-ref
     outcome-import-ref
@@ -52,3 +55,4 @@ Class Reference
     tab-ref
     upload-ref
     user-ref
+    usage-rights-ref
diff --git a/docs/comm-message-ref.rst b/docs/comm-message-ref.rst
new file mode 100644
index 0000000..b39c349
--- /dev/null
+++ b/docs/comm-message-ref.rst
@@ -0,0 +1,6 @@
+===========
+CommMessage
+===========
+
+.. autoclass:: canvasapi.comm_message.CommMessage
+    :members:
diff --git a/docs/examples.rst b/docs/examples.rst
index 08ef5e3..4170381 100644
--- a/docs/examples.rst
+++ b/docs/examples.rst
@@ -370,3 +370,42 @@ Using :func:`canvasapi.assignment.Assignment.delete`:
 .. code-block:: python
 
     assignment.delete()
+
+
+Update (Edit) an Assignment Submission
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Using :func:`canvasapi.submission.Submission.edit`:
+
+Change a User's Assignment Score
+################################
+
+.. code-block:: python
+
+    # Set `score` to a Int value
+
+    submission = assignment.get_submission(student_id)
+    submission.edit(submission={'posted_grade':score})
+
+Add n Points to All Users' Assignments
+######################################
+
+.. code-block:: python
+
+    # Set added points as an Int variable
+    
+    added_points = 2
+
+    submissions = assignment.get_submissions()
+    
+    for submission in submissions:
+
+        # Handle an unscored assignment by checking the `score` value
+
+        if submission.score is not None:
+            score = submission.score + added_points
+        else:
+            # Treat no submission as 0 points
+            score = 0 + added_points
+        
+        submission.edit(submission={'posted_grade': score})
diff --git a/docs/license-ref.rst b/docs/license-ref.rst
new file mode 100644
index 0000000..17eaa79
--- /dev/null
+++ b/docs/license-ref.rst
@@ -0,0 +1,6 @@
+=======
+License
+=======
+
+.. autoclass:: canvasapi.license.License
+    :members:
\ No newline at end of file
diff --git a/docs/quiz-ref.rst b/docs/quiz-ref.rst
index 49c88ad..74a24a4 100644
--- a/docs/quiz-ref.rst
+++ b/docs/quiz-ref.rst
@@ -19,6 +19,13 @@ QuizQuestion
 .. autoclass:: canvasapi.quiz.QuizQuestion
     :members:
 
+==========
+QuizReport
+==========
+
+.. autoclass:: canvasapi.quiz.QuizReport
+    :members:
+
 ==============
 QuizSubmission
 ==============
diff --git a/docs/usage-rights-ref.rst b/docs/usage-rights-ref.rst
new file mode 100644
index 0000000..efeb780
--- /dev/null
+++ b/docs/usage-rights-ref.rst
@@ -0,0 +1,6 @@
+===========
+UsageRights
+===========
+
+.. autoclass:: canvasapi.usage_rights.UsageRights
+    :members:
\ No newline at end of file


Test patch:
diff --git a/tests/fixtures/account.json b/tests/fixtures/account.json
index 1c4302f..af424bc 100644
--- a/tests/fixtures/account.json
+++ b/tests/fixtures/account.json
@@ -195,6 +195,16 @@
 			"default_time_zone": "America/New_York"
 		}
 	},
+	"get_brand_variables": {
+		"method": "GET",
+		"endpoint": "brand_variables",
+		"data": {
+			"ic-brand-primary-darkened-5": "#343637",
+			"ic-brand-primary-darkened-10": "#313334",
+			"ic-brand-primary-darkened-15": "#2E3031",
+			"ic-brand-primary-lightened-5": "#404142"
+		}
+	},
 	"get_courses": {
 		"method": "GET",
 		"endpoint": "accounts/1/courses",
@@ -1330,7 +1340,7 @@
         	"path": "/api/v1/users/self/course_nicknames/:course_id",
         	"scope": "url:GET|/api/v1/users/self/course_nicknames/:course_id",
         	"resource": "users",
-        	"resource_name": "Users"	
+        	"resource_name": "Users"
 		}
 	],
 	"status_code": 200
@@ -1732,5 +1742,22 @@
 			"state": "on"
 		},
 		"status_code": 200
+	},
+	"get_authentication_events": {
+		"method": "GET",
+		"endpoint": "audit/authentication/accounts/1?per_page=100",
+		"data": [
+			{
+				"created_at": "2012-07-19T15:00:00-06:00",
+				"event_type": "login",
+				"pseudonym_id": 9478
+			},
+			{
+				"created_at": "2012-07-20T15:00:00-06:00",
+				"event_type": "logout",
+				"pseudonym_id": 9478
+			}
+		],
+		"status_code": 200
 	}
 }
diff --git a/tests/fixtures/assignment.json b/tests/fixtures/assignment.json
index 9399ac3..e457e63 100644
--- a/tests/fixtures/assignment.json
+++ b/tests/fixtures/assignment.json
@@ -255,6 +255,27 @@
 		},
 		"status_code": 200
 	},
+	"get_assignment_with_overrides": {
+		"method": "GET",
+		"endpoint": "courses/1/assignments/1",
+		"data": {
+			"id": 1,
+			"course_id": 1,
+			"name": "Assignment 1",
+			"description": "Do this assignment",
+			"overrides": [
+				{
+					"id": 1,
+					"assignment_id": 1,
+					"student_ids": [1],
+					"group_id": 1,
+					"course_section_id": 1,
+					"title": "Assignment Override 1"
+				}
+			]
+		},
+		"status_code": 200
+	},
 	"override_group_alias": {
 		"method": "GET",
 		"endpoint": "groups/1/assignments/1/override",
@@ -275,6 +296,24 @@
 			"title": "Section Assignment Override"
 		}
 	},
+	"set_extensions":{
+		"method": "POST",
+		"endpoint": "courses/1/assignments/1/extensions",
+		"data": {
+			"assignment_extensions": [
+				{
+					"assignment_id": 2,
+					"user_id": 3,
+					"extra_attempts": 2
+				},
+				{
+					"assignment_id": 2,
+					"user_id": 2,
+					"extra_attempts": 2
+				}
+			]
+		}
+	},
 	"submit": {
 		"method": "POST",
 		"endpoint": "courses/1/assignments/1/submissions",
diff --git a/tests/fixtures/comm_message.json b/tests/fixtures/comm_message.json
new file mode 100644
index 0000000..d63bbcc
--- /dev/null
+++ b/tests/fixtures/comm_message.json
@@ -0,0 +1,34 @@
+{
+  "comm_messages": {
+    "method": "GET",
+    "endpoint": "comm_messages",
+    "data": [{
+        "id": 42,
+        "created_at": "2013-03-19T21:00:00Z",
+        "sent_at": "2013-03-20T22:42:00Z",
+        "workflow_state": "sent",
+        "from": "notifications@example.com",
+        "from_name": "Instructure Canvas",
+        "to": "someone@example.com",
+        "reply_to": "notifications+specialdata@example.com",
+        "subject": "example subject line",
+        "body": "This is the body of the message",
+        "html_body": "<html><body>This is the body of the message</body></html>"
+      },
+      {
+        "id": 2,
+        "created_at": "2013-03-19T21:00:00Z",
+        "sent_at": "2013-03-20T22:42:00Z",
+        "workflow_state": "sent",
+        "from": "johndoe@ucf.edu",
+        "from_name": "John Doe",
+        "to": "janedoe@ucf.edu",
+        "reply_to": "notifications+specialdata@example.com",
+        "subject": "My Subject",
+        "body": "My Message Body",
+        "html_body": "<html><body>My Message Body</body></html>"
+      }
+    ],
+    "status_code": 200
+  }
+}
diff --git a/tests/fixtures/course.json b/tests/fixtures/course.json
index 53b5e70..0fab725 100644
--- a/tests/fixtures/course.json
+++ b/tests/fixtures/course.json
@@ -146,6 +146,25 @@
 		},
 		"status_code": 200
 	},
+	"get_assignments_for_group": {
+		"method": "GET",
+		"endpoint": "courses/1/assignment_groups/5/assignments",
+		"data": [
+			{
+				"id": 3,
+				"course_id": 1,
+				"name": "Assignment 1",
+				"description": "Do this assignment"
+			},
+			{
+				"id": 4,
+				"course_id": 1,
+				"name": "Assignment 2",
+				"description": "Do this assignment too"
+			}
+		],
+		"status_code": 200
+	},
 	"get_blueprint": {
 		"method": "GET",
 		"endpoint": "courses/1/blueprint_templates/1",
@@ -514,6 +533,38 @@
 		],
 		"status_code": 200
 	},
+	"get_submission_history": {
+		"method": "GET",
+		"endpoint": "courses/1/gradebook_history/08-23-2019/graders/1/assignments/1/submissions",
+		"data": [
+			{
+                "submission_id": 4,
+                "versions": null
+            },
+            {
+                "submission_id": 7,
+                "versions": null
+            }
+        ]
+    },
+	"get_uncollated_submissions": {
+		"method": "GET",
+		"endpoint": "courses/1/gradebook_history/feed",
+		"data": [
+			{
+				"id": 1,
+				"assignment_id": 40,
+				"assignment_name": "assignment1",
+				"grader_id": 53
+			},
+			{
+				"id": 2,
+				"assignment_id": 22,
+				"assignment_name": "assignment2",
+				"grader_id": 67
+			}
+		]
+	},
 	"get_user": {
 		"method": "GET",
 		"endpoint": "courses/1/users/1",
@@ -1652,6 +1703,59 @@
 		],
 		"status_code": 200
 	},
+	"get_gradebook_history_dates": {
+		"method": "GET",
+		"endpoint": "courses/1/gradebook_history/days",
+		"data": [
+			{
+				"date": "2018-08-09",
+				"graders": [
+					{
+						"name": "John Smith", 
+						"id": 49, 
+						"assignments":[
+							{
+								"id": 44, 
+								"description": "<p>homework1</p>"
+							}
+						]
+					}
+				]
+			},
+			{
+				"date": "2019-10-24",
+				"graders": [
+					{
+						"name": "Sara Hanks", 
+						"id": 32, 
+						"assignments":[
+							{
+								"id": 19, 
+								"description": "<p>homework2</p>"
+							}
+						]
+					}
+				]
+			}
+		]
+
+	},
+	"get_gradebook_history_details": {
+		"method": "GET",
+		"endpoint": "courses/1/gradebook_history/03-26-2019",
+		"data": [
+			{
+				"id": 27,
+				"name": "John Smith",
+				"assignments": [1, 2, 3]
+			},
+			{
+				"id": 30,
+				"name": "Jenny Parker",
+				"assignments": [4, 5, 6]
+			}
+		]
+	},
 	"get_single_grading_standard": {
 		"method": "GET",
 		"endpoint": "courses/1/grading_standards/1",
@@ -1910,5 +2014,51 @@
 			}
 		},
 		"status_code": 200
+	},
+	"create_rubric_association": {
+		"method": "POST",
+		"endpoint": "courses/1/rubric_associations",
+		"data": {
+			"id": 4,
+			"association_type": "Course"
+		},
+		"status_code": 200
+	},
+	"set_usage_rights": {
+		"method": "PUT",
+		"endpoint": "courses/1/usage_rights",
+		"data": {
+			"use_justification": "fair_use",
+			"license": "private",
+			"message": "2 files updated",
+			"file_ids": [1,2]
+		},
+		"status_code": 200
+	},
+	"remove_usage_rights": {
+		"method": "DELETE",
+		"endpoint": "courses/1/usage_rights",
+		"data": {
+			"message": "2 files updated",
+			"file_ids": [1,2]
+		},
+		"status_code": 200
+	},
+	"get_licenses": {
+		"method": "GET",
+		"endpoint": "courses/1/content_licenses",
+		"data": [
+			{
+				"id": "private",
+				"name": "Private (Copyrighted)",
+				"url": "http://en.wikipedia.org/wiki/Copyright"
+			},
+			{
+				"id": "public_domain",
+				"name": "Public domain",
+				"url": "http://en.wikipedia.org/wiki/Public_domain"
+			}
+		],
+		"status_code": 200
 	}
 }
diff --git a/tests/fixtures/group.json b/tests/fixtures/group.json
index f34d68f..57af19e 100644
--- a/tests/fixtures/group.json
+++ b/tests/fixtures/group.json
@@ -946,5 +946,42 @@
 			"export_type": "d"
 		},
 		"status_code": 200
+	},
+	"set_usage_rights": {
+		"method": "PUT",
+		"endpoint": "groups/1/usage_rights",
+		"data": {
+			"use_justification": "fair_use",
+			"license": "private",
+			"message": "2 files updated",
+			"file_ids": [1,2]
+		},
+		"status_code": 200
+	},
+	"remove_usage_rights": {
+		"method": "DELETE",
+		"endpoint": "groups/1/usage_rights",
+		"data": {
+			"message": "2 files updated",
+			"file_ids": [1,2]
+		},
+		"status_code": 200
+	},
+	"get_licenses": {
+		"method": "GET",
+		"endpoint": "groups/1/content_licenses",
+		"data": [
+			{
+				"id": "private",
+				"name": "Private (Copyrighted)",
+				"url": "http://en.wikipedia.org/wiki/Copyright"
+			},
+			{
+				"id": "public_domain",
+				"name": "Public domain",
+				"url": "http://en.wikipedia.org/wiki/Public_domain"
+			}
+		],
+		"status_code": 200
 	}
 }
diff --git a/tests/fixtures/login.json b/tests/fixtures/login.json
index 8942470..53ffe6d 100644
--- a/tests/fixtures/login.json
+++ b/tests/fixtures/login.json
@@ -7,7 +7,7 @@
 			"id": 101,
 			"unique_id": "belieber@example.com",
 			"user_id": 1
-		}, 
+		},
 		"status_code": 200
 	},
 	"list_user_logins": {
@@ -62,5 +62,22 @@
 			"unique_id": "belieber@example.com",
 			"user_id": 1
 		}
+	},
+	"get_authentication_events": {
+		"method": "GET",
+		"endpoint": "audit/authentication/logins/101",
+		"data": [
+			{
+				"created_at": "2012-07-19T15:00:00-06:00",
+				"event_type": "login",
+				"pseudonym_id": 9478
+			},
+			{
+				"created_at": "2012-07-20T15:00:00-06:00",
+				"event_type": "logout",
+				"pseudonym_id": 9478
+			}
+		],
+		"status_code": 200
 	}
 }
diff --git a/tests/fixtures/quiz.json b/tests/fixtures/quiz.json
index f2bf6b7..22e753a 100644
--- a/tests/fixtures/quiz.json
+++ b/tests/fixtures/quiz.json
@@ -1,4 +1,24 @@
 {
+	"abort_or_delete_report": {
+		"method": "DELETE",
+		"endpoint": "courses/1/quizzes/1/reports/1",
+		"data": "True",
+		"status_code": 204
+	},
+	"abort_or_delete_report_failure": {
+		"method": "DELETE",
+		"endpoint": "courses/1/quizzes/1/reports/1",
+		"data": "False",
+		"status_code": 200
+	},
+	"broadcast_message": {
+		"method":"POST",
+		"endpoint": "courses/1/quizzes/1/submission_users/message",
+		"data": {
+			"status": "created"
+		},
+		"status_code": 201
+	},
 	"delete": {
 		"method":"DELETE",
 		"endpoint": "courses/1/quizzes/1",
@@ -118,22 +138,22 @@
 		"data": {
 			"quiz_submissions": [
 				{
-						"id": 1,
-						"quiz_id": 1,
-						"user_id": 1,
-						"submission_id": 1,
-						"attempt": 3,
-						"manually_unlocked": null,
-						"score": 7
+					"id": 1,
+					"quiz_id": 1,
+					"user_id": 1,
+					"submission_id": 1,
+					"attempt": 3,
+					"manually_unlocked": null,
+					"score": 7
 				},
 				{
-						"id": 2,
-						"quiz_id": 1,
-						"user_id": 1,
-						"submission_id": 2,
-						"attempt": 3,
-						"manually_unlocked": null,
-						"score": 5
+					"id": 2,
+					"quiz_id": 1,
+					"user_id": 1,
+					"submission_id": 2,
+					"attempt": 3,
+					"manually_unlocked": null,
+					"score": 5
 				}
 			]
 		}
@@ -203,6 +223,26 @@
 		},
 		"status_code": 200
 	},
+	"get_submission_events": {
+		"method": "GET",
+		"endpoint": "courses/1/quizzes/1/submissions/1/events",
+		"data": {
+			"quiz_submission_events": [
+				{
+					"id": "3409",
+					"event_type": "page_blurred",
+					"event_data": null,
+					"created_at": "2014-11-16T13:37:21Z"
+				},
+				{
+					"id": "3410",
+					"event_type": "page_focused",
+					"event_data": null,
+					"created_at": "2014-11-16T13:37:27Z"
+				}
+			]
+		}
+	},
 	"set_extensions": {
 		"method": "POST",
 		"endpoint": "courses/1/quizzes/1/extensions",
@@ -226,5 +266,52 @@
 				}
 			]
 		}
+	},
+	"submit_events": {
+		"method": "POST",
+		"endpoint": "courses/1/quizzes/1/submissions/1/events",
+		"data": {},
+		"status_code": 204
+	},
+	"get_all_quiz_reports": {
+		"method": "GET",
+		"endpoint": "courses/1/quizzes/1/reports",
+		"data": [
+			{
+				"id": 1,
+				"quiz_id": 1,
+				"report_type": 1,
+				"includes_all_versions": true
+			},
+			{
+				"id": 2,
+				"quiz_id": 1,
+				"report_type": 1,
+				"includes_all_versions": false
+			}
+		],
+		"status_code": 200
+	},
+	"get_quiz_report": {
+		"method": "GET",
+		"endpoint": "courses/1/quizzes/1/reports/1",
+		"data": {
+			"id": 1,
+			"quiz_id": 1,
+			"report_type": "student_analysis",
+			"includes_all_versions": true
+		},
+		"status_code": 200
+	},
+	"create_report": {
+		"method": "POST",
+		"endpoint": "courses/1/quizzes/1/reports",
+		"data": {
+			"id": 1,
+			"quiz_id": 1,
+			"report_type": "student_analysis",
+			"includes_all_paramaters": true
+		},
+		"status_code": 200
 	}
 }
diff --git a/tests/fixtures/rubric.json b/tests/fixtures/rubric.json
new file mode 100644
index 0000000..78f8fcd
--- /dev/null
+++ b/tests/fixtures/rubric.json
@@ -0,0 +1,20 @@
+{
+	"update_rubric_association": {
+		"method": "PUT",
+		"endpoint": "courses/1/rubric_associations/4",
+		"data": {
+			"id": 5,
+			"association_type": "Assignment"
+		},
+		"status_code": 200
+	},
+	"delete_rubric_association": {
+		"method": "DELETE",
+		"endpoint": "courses/1/rubric_associations/4",
+		"data": {
+			"id": 4,
+			"association_type": "Assignment"
+		},
+		"status_code": 200
+	}
+}
diff --git a/tests/fixtures/submission.json b/tests/fixtures/submission.json
index 290955a..2208e20 100644
--- a/tests/fixtures/submission.json
+++ b/tests/fixtures/submission.json
@@ -218,4 +218,4 @@
 			]
 		}
 	}
-}
\ No newline at end of file
+}
diff --git a/tests/fixtures/user.json b/tests/fixtures/user.json
index 6c4129a..2a41536 100644
--- a/tests/fixtures/user.json
+++ b/tests/fixtures/user.json
@@ -918,5 +918,59 @@
 			"state": "allowed"
 		},
 		"status_code": 200
+	},
+	"get_authentication_events": {
+		"method": "GET",
+		"endpoint": "audit/authentication/users/1",
+		"data": [
+			{
+				"created_at": "2012-07-19T15:00:00-06:00",
+				"event_type": "login",
+				"pseudonym_id": 9478
+			},
+			{
+				"created_at": "2012-07-20T15:00:00-06:00",
+				"event_type": "logout",
+				"pseudonym_id": 9478
+			}
+		],
+		"status_code": 200
+	},
+	"set_usage_rights": {
+		"method": "PUT",
+		"endpoint": "users/1/usage_rights",
+		"data": {
+			"use_justification": "fair_use",
+			"license": "private",
+			"message": "2 files updated",
+			"file_ids": [1,2]
+		},
+		"status_code": 200
+	},
+	"remove_usage_rights": {
+		"method": "DELETE",
+		"endpoint": "users/1/usage_rights",
+		"data": {
+			"message": "2 files updated",
+			"file_ids": [1,2]
+		},
+		"status_code": 200
+	},
+	"get_licenses": {
+		"method": "GET",
+		"endpoint": "users/1/content_licenses",
+		"data": [
+			{
+				"id": "private",
+				"name": "Private (Copyrighted)",
+				"url": "http://en.wikipedia.org/wiki/Copyright"
+			},
+			{
+				"id": "public_domain",
+				"name": "Public domain",
+				"url": "http://en.wikipedia.org/wiki/Public_domain"
+			}
+		],
+		"status_code": 200
 	}
 }
diff --git a/tests/test_account.py b/tests/test_account.py
index 098d6e5..e79d275 100644
--- a/tests/test_account.py
+++ b/tests/test_account.py
@@ -16,6 +16,7 @@ from canvasapi.account import (
     SSOSettings,
 )
 from canvasapi.authentication_provider import AuthenticationProvider
+from canvasapi.authentication_event import AuthenticationEvent
 from canvasapi.course import Course
 from canvasapi.enrollment import Enrollment
 from canvasapi.enrollment_term import EnrollmentTerm
@@ -1332,6 +1333,23 @@ class TestAccount(unittest.TestCase):
         with self.assertRaises(RequiredFieldMissing):
             self.AccountNotification.update_global_notification(notif)
 
+    # get_authentication_events()
+    def test_get_authentication_events(self, m):
+        register_uris({"account": ["get_authentication_events"]}, m)
+
+        authentication_event = self.account.get_authentication_events()
+        event_list = [event for event in authentication_event]
+
+        self.assertEqual(len(event_list), 2)
+
+        self.assertIsInstance(event_list[0], AuthenticationEvent)
+        self.assertEqual(event_list[0].event_type, "login")
+        self.assertEqual(event_list[0].pseudonym_id, 9478)
+
+        self.assertIsInstance(event_list[1], AuthenticationEvent)
+        self.assertEqual(event_list[1].created_at, "2012-07-20T15:00:00-06:00")
+        self.assertEqual(event_list[1].event_type, "logout")
+
 
 @requests_mock.Mocker()
 class TestAccountNotification(unittest.TestCase):
diff --git a/tests/test_assignment.py b/tests/test_assignment.py
index 13bca61..3ddab02 100644
--- a/tests/test_assignment.py
+++ b/tests/test_assignment.py
@@ -5,7 +5,12 @@ import uuid
 import requests_mock
 
 from canvasapi import Canvas
-from canvasapi.assignment import Assignment, AssignmentGroup, AssignmentOverride
+from canvasapi.assignment import (
+    Assignment,
+    AssignmentGroup,
+    AssignmentOverride,
+    AssignmentExtension,
+)
 from canvasapi.exceptions import CanvasException, RequiredFieldMissing
 from canvasapi.peer_review import PeerReview
 from canvasapi.progress import Progress
@@ -26,6 +31,16 @@ class TestAssignment(unittest.TestCase):
             self.course = self.canvas.get_course(1)
             self.assignment = self.course.get_assignment(1)
 
+    def test__init__overrides(self, m):
+        register_uris({"assignment": ["get_assignment_with_overrides"]}, m)
+
+        assignment = self.course.get_assignment(1)
+
+        self.assertTrue(hasattr(assignment, "overrides"))
+        self.assertIsInstance(assignment.overrides, list)
+        self.assertEqual(len(assignment.overrides), 1)
+        self.assertIsInstance(assignment.overrides[0], AssignmentOverride)
+
     # create_override()
     def test_create_override(self, m):
         register_uris({"assignment": ["create_override"]}, m)
@@ -130,6 +145,43 @@ class TestAssignment(unittest.TestCase):
         self.assertEqual(len(submission_list_by_id), 2)
         self.assertIsInstance(submission_list_by_id[0], Submission)
 
+    # set_extensions()
+    def test_set_extensions(self, m):
+        register_uris({"assignment": ["set_extensions"]}, m)
+
+        extension = self.assignment.set_extensions(
+            [{"user_id": 3, "extra_attempts": 2}, {"user_id": 2, "extra_attempts": 2}]
+        )
+
+        self.assertIsInstance(extension, list)
+        self.assertEqual(len(extension), 2)
+
+        self.assertIsInstance(extension[0], AssignmentExtension)
+        self.assertEqual(extension[0].user_id, 3)
+        self.assertTrue(hasattr(extension[0], "extra_attempts"))
+        self.assertEqual(extension[0].extra_attempts, 2)
+
+        self.assertIsInstance(extension[1], AssignmentExtension)
+        self.assertEqual(extension[1].user_id, 2)
+        self.assertTrue(hasattr(extension[1], "extra_attempts"))
+        self.assertEqual(extension[1].extra_attempts, 2)
+
+    def test_set_extensions_not_list(self, m):
+        with self.assertRaises(ValueError):
+            self.assignment.set_extensions({"user_id": 3, "exrra_attempts": 2})
+
+    def test_set_extensions_empty_list(self, m):
+        with self.assertRaises(ValueError):
+            self.assignment.set_extensions([])
+
+    def test_set_extensions_non_dicts(self, m):
+        with self.assertRaises(ValueError):
+            self.assignment.set_extensions([("user_id", 1), ("extra_attempts", 2)])
+
+    def test_set_extensions_missing_key(self, m):
+        with self.assertRaises(RequiredFieldMissing):
+            self.assignment.set_extensions([{"extra_attempts": 3}])
+
     # submit()
     def test_submit(self, m):
         register_uris({"assignment": ["submit"]}, m)
@@ -237,6 +289,22 @@ class TestAssignment(unittest.TestCase):
             cleanup_file(filename)
 
 
+@requests_mock.Mocker()
+class TestAssignmentExtension(unittest.TestCase):
+    def setUp(self):
+        self.canvas = Canvas(settings.BASE_URL, settings.API_KEY)
+
+        self.extension = AssignmentExtension(
+            self.canvas._Canvas__requester,
+            {"assignment_id": 2, "user_id": 3, "extra_attempts": 2},
+        )
+
+    # __str__()
+    def test__str__(self, m):
+        string = str(self.extension)
+        self.assertIsInstance(string, str)
+
+
 @requests_mock.Mocker()
 class TestAssignmentGroup(unittest.TestCase):
     def setUp(self):
diff --git a/tests/test_authentication_event.py b/tests/test_authentication_event.py
new file mode 100644
index 0000000..2849060
--- /dev/null
+++ b/tests/test_authentication_event.py
@@ -0,0 +1,39 @@
+from __future__ import absolute_import, division, print_function, unicode_literals
+import unittest
+
+import requests_mock
+
+from canvasapi import Canvas
+from tests import settings
+from tests.util import register_uris
+
+
+@requests_mock.Mocker()
+class TestAuthenticationEvent(unittest.TestCase):
+    def setUp(self):
+        self.canvas = Canvas(settings.BASE_URL, settings.API_KEY)
+
+        with requests_mock.Mocker() as m:
+            requires = {
+                "account": ["get_by_id", "get_authentication_events"],
+                "login": ["create_user_login", "get_authentication_events"],
+                "user": ["get_by_id", "get_authentication_events"],
+            }
+            register_uris(requires, m)
+
+            self.account = self.canvas.get_account(1)
+            self.login = self.account.create_user_login(
+                user={"id": 1}, login={"unique_id": "belieber@example.com"}
+            )
+            self.user = self.canvas.get_user(1)
+
+            self.authentication_event_account = self.account.get_authentication_events()[
+                0
+            ]
+            self.authentication_event_login = self.login.get_authentication_events()[0]
+            self.authentication_event_user = self.user.get_authentication_events()[0]
+
+    # __str__()
+    def test__str__(self, m):
+        string = str(self.authentication_event_account)
+        self.assertIsInstance(string, str)
diff --git a/tests/test_canvas.py b/tests/test_canvas.py
index 458759e..7a5a78d 100644
--- a/tests/test_canvas.py
+++ b/tests/test_canvas.py
@@ -25,6 +25,7 @@ from canvasapi.paginated_list import PaginatedList
 from canvasapi.progress import Progress
 from canvasapi.section import Section
 from canvasapi.user import User
+from canvasapi.comm_message import CommMessage
 from tests import settings
 from tests.util import register_uris
 
@@ -70,6 +71,10 @@ class TestCanvas(unittest.TestCase):
                 ),
             )
 
+    def test_init_strips_extra_spaces_in_api_key(self, m):
+        client = Canvas(settings.BASE_URL, " 12345 ")
+        self.assertEqual(client._Canvas__requester.access_token, "12345")
+
     # create_account()
     def test_create_account(self, m):
         register_uris({"account": ["create"]}, m)
@@ -123,6 +128,13 @@ class TestCanvas(unittest.TestCase):
         account_list = [account for account in accounts]
         self.assertEqual(len(account_list), 2)
 
+    # get_brand_variables()
+    def test_get_brand_variables(self, m):
+        register_uris({"account": ["get_brand_variables"]}, m)
+
+        variables = self.canvas.get_brand_variables()
+        self.assertIsInstance(variables, dict)
+
     # get_course()
     def test_get_course(self, m):
         register_uris({"course": ["get_by_id"]}, m)
@@ -802,3 +814,21 @@ class TestCanvas(unittest.TestCase):
         self.assertEqual(epub2["id"], 2)
         self.assertEqual(epub1["workflow_state"], "exported")
         self.assertEqual(epub2["workflow_state"], "exported")
+
+    # comm_messages()
+    def test_get_comm_messages(self, m):
+        register_uris({"comm_message": ["comm_messages"]}, m)
+
+        comm_messages = self.canvas.get_comm_messages(2)
+
+        self.assertIsInstance(comm_messages, PaginatedList)
+        self.assertIsInstance(comm_messages[0], CommMessage)
+        self.assertIsInstance(comm_messages[1], CommMessage)
+
+        self.assertTrue(hasattr(comm_messages[0], "id"))
+        self.assertTrue(hasattr(comm_messages[1], "body"))
+
+        self.assertEqual(comm_messages[0].id, 42)
+        self.assertEqual(comm_messages[0].subject, "example subject line")
+        self.assertEqual(comm_messages[1].id, 2)
+        self.assertEqual(comm_messages[1].subject, "My Subject")
diff --git a/tests/test_canvas_object.py b/tests/test_canvas_object.py
index 6e3266d..405f295 100644
--- a/tests/test_canvas_object.py
+++ b/tests/test_canvas_object.py
@@ -1,5 +1,6 @@
 from __future__ import absolute_import, division, print_function, unicode_literals
 import unittest
+import warnings
 
 from canvasapi.canvas_object import CanvasObject
 
@@ -8,13 +9,22 @@ class TestCanvasObject(unittest.TestCase):
 
     # to_json()
     def test_canvas_object_to_json(self):
-        attributes = {"name": "Test Object", "id": 1}
-        canvas_obj = CanvasObject(None, attributes)
+        warnings.simplefilter("always", DeprecationWarning)
 
-        prev_json = canvas_obj.to_json()
-        self.assertIsInstance(prev_json, str)
+        with warnings.catch_warnings(record=True) as warning_list:
+            attributes = {"name": "Test Object", "id": 1}
+            canvas_obj = CanvasObject(None, attributes)
 
-        attributes.update({"name": "Test Object 2"})
-        canvas_obj.set_attributes(attributes)
+            prev_json = canvas_obj.to_json()
+            self.assertIsInstance(prev_json, str)
 
-        self.assertNotEqual(canvas_obj.to_json(), prev_json)
+            attributes.update({"name": "Test Object 2"})
+            canvas_obj.set_attributes(attributes)
+
+            self.assertNotEqual(canvas_obj.to_json(), prev_json)
+
+        self.assertEqual(len(warning_list), 4)
+        self.assertEqual(warning_list[0].category, DeprecationWarning)
+        self.assertEqual(warning_list[1].category, DeprecationWarning)
+        self.assertEqual(warning_list[2].category, DeprecationWarning)
+        self.assertEqual(warning_list[3].category, DeprecationWarning)
diff --git a/tests/test_comm_messages.py b/tests/test_comm_messages.py
new file mode 100644
index 0000000..05d99de
--- /dev/null
+++ b/tests/test_comm_messages.py
@@ -0,0 +1,25 @@
+from __future__ import absolute_import, division, print_function, unicode_literals
+import unittest
+
+import requests_mock
+
+from canvasapi import Canvas
+from tests import settings
+from tests.util import register_uris
+
+
+@requests_mock.Mocker()
+class TestCommMessage(unittest.TestCase):
+    def setUp(self):
+        self.canvas = Canvas(settings.BASE_URL, settings.API_KEY)
+
+        with requests_mock.Mocker() as m:
+            requires = {"comm_message": ["comm_messages"]}
+            register_uris(requires, m)
+
+            self.comm_message = self.canvas.get_comm_messages(2)[0]
+
+    # __str__()
+    def test__str__(self, m):
+        string = str(self.comm_message)
+        self.assertIsInstance(string, str)
diff --git a/tests/test_course.py b/tests/test_course.py
index 5080088..b07c9ba 100644
--- a/tests/test_course.py
+++ b/tests/test_course.py
@@ -14,6 +14,12 @@ from canvasapi.blueprint import BlueprintSubscription
 from canvasapi.blueprint import BlueprintTemplate
 from canvasapi.course import Course, CourseNickname, Page
 from canvasapi.discussion_topic import DiscussionTopic
+from canvasapi.gradebook_history import (
+    Day,
+    Grader,
+    SubmissionVersion,
+    SubmissionHistory,
+)
 from canvasapi.grading_standard import GradingStandard
 from canvasapi.enrollment import Enrollment
 from canvasapi.course_epub_export import CourseEpubExport
@@ -25,18 +31,20 @@ from canvasapi.file import File
 from canvasapi.folder import Folder
 from canvasapi.grading_period import GradingPeriod
 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 import OutcomeImport
 from canvasapi.paginated_list import PaginatedList
 from canvasapi.progress import Progress
 from canvasapi.quiz import Quiz, QuizExtension
-from canvasapi.rubric import Rubric
+from canvasapi.rubric import Rubric, RubricAssociation
 from canvasapi.section import Section
 from canvasapi.submission import GroupedSubmission, Submission
 from canvasapi.tab import Tab
 from canvasapi.user import User
 from canvasapi.user import UserDisplay
+from canvasapi.usage_rights import UsageRights
 from canvasapi.content_migration import ContentMigration, Migrator
 from canvasapi.content_export import ContentExport
 from tests import settings
@@ -137,6 +145,16 @@ class TestCourse(unittest.TestCase):
         self.assertIsInstance(updated_list[0], AssignmentOverride)
         self.assertIsInstance(updated_list[1], AssignmentOverride)
 
+    # get_uncollated_submissions()
+    def test_get_uncollated_submissions(self, m):
+        register_uris({"course": ["get_uncollated_submissions"]}, m)
+
+        u_submissions = self.course.get_uncollated_submissions()
+        u_sub_list = [sub for sub in u_submissions]
+        self.assertEqual(len(u_sub_list), 2)
+        self.assertIsInstance(u_sub_list[0], SubmissionVersion)
+        self.assertIsInstance(u_sub_list[1], SubmissionVersion)
+
     # get_user()
     def test_get_user(self, m):
         register_uris({"course": ["get_user"]}, m)
@@ -552,6 +570,28 @@ class TestCourse(unittest.TestCase):
             self.assertEqual(len(warning_list), 1)
             self.assertEqual(warning_list[-1].category, DeprecationWarning)
 
+    # get_gradebook_history_dates()
+    def test_get_gradebook_history_dates(self, m):
+        register_uris({"course": ["get_gradebook_history_dates"]}, m)
+
+        gradebook_history = self.course.get_gradebook_history_dates()
+        gh_list = [gh for gh in gradebook_history]
+        self.assertEqual(len(gh_list), 2)
+        self.assertIsInstance(gh_list[0], Day)
+        self.assertIsInstance(gh_list[1], Day)
+
+    # get_gradebook_history_details
+    def test_get_gradebook_history_details(self, m):
+        register_uris({"course": ["get_gradebook_history_details"]}, m)
+
+        gradebook_history_details = self.course.get_gradebook_history_details(
+            "03-26-2019"
+        )
+        ghd_list = [ghd for ghd in gradebook_history_details]
+        self.assertEqual(len(ghd_list), 2)
+        self.assertIsInstance(ghd_list[0], Grader)
+        self.assertIsInstance(ghd_list[1], Grader)
+
     # get_groups()
     def test_get_groups(self, m):
         requires = {"course": ["list_groups_context", "list_groups_context2"]}
@@ -721,6 +761,43 @@ class TestCourse(unittest.TestCase):
         self.assertTrue(hasattr(assignment_group_by_obj, "course_id"))
         self.assertEqual(assignment_group_by_obj.course_id, 1)
 
+    # get_assignments_for_group()
+    def test_get_assignments_for_group(self, m):
+        register_uris(
+            {
+                "course": ["get_assignments_for_group"],
+                "assignment": ["get_assignment_group"],
+            },
+            m,
+        )
+
+        assignment_group_obj = self.course.get_assignment_group(5)
+        response = self.course.get_assignments_for_group(5)
+        assignments = [assignment for assignment in response]
+
+        self.assertIsInstance(response, PaginatedList)
+
+        for assignment in assignments:
+            self.assertIsInstance(assignment, Assignment)
+            self.assertTrue(hasattr(assignment, "id"))
+            self.assertTrue(hasattr(assignment, "name"))
+            self.assertTrue(hasattr(assignment, "course_id"))
+            self.assertTrue(hasattr(assignment, "description"))
+            self.assertEqual(assignment.course_id, self.course.id)
+
+        response = self.course.get_assignments_for_group(assignment_group_obj)
+        assignments = [assignment for assignment in response]
+
+        self.assertIsInstance(response, PaginatedList)
+
+        for assignment in assignments:
+            self.assertIsInstance(assignment, Assignment)
+            self.assertTrue(hasattr(assignment, "id"))
+            self.assertTrue(hasattr(assignment, "name"))
+            self.assertTrue(hasattr(assignment, "course_id"))
+            self.assertTrue(hasattr(assignment, "description"))
+            self.assertEqual(assignment.course_id, self.course.id)
+
     # list_assignment_groups()
     def test_list_assignment_groups(self, m):
         register_uris(
@@ -984,6 +1061,16 @@ class TestCourse(unittest.TestCase):
             self.assertEqual(len(warning_list), 1)
             self.assertEqual(warning_list[-1].category, DeprecationWarning)
 
+    # get_submission_history
+    def test_get_submission_history(self, m):
+        register_uris({"course": ["get_submission_history"]}, m)
+
+        submissions = self.course.get_submission_history("08-23-2019", 1, 1)
+        sub_list = [sub for sub in submissions]
+        self.assertEqual(len(sub_list), 2)
+        self.assertIsInstance(sub_list[0], SubmissionHistory)
+        self.assertIsInstance(sub_list[1], SubmissionHistory)
+
     # update_submission()
     def test_update_submission(self, m):
         register_uris(
@@ -1769,6 +1856,58 @@ class TestCourse(unittest.TestCase):
         self.assertEqual(rubric["rubric_association"].rubric_id, 1)
         self.assertEqual(rubric["rubric_association"].association_type, "Course")
 
+    # create_rubric_association()
+    def test_create_rubric_association(self, m):
+        register_uris({"course": ["create_rubric_association"]}, m)
+
+        rubric_association = self.course.create_rubric_association()
+
+        self.assertIsInstance(rubric_association, RubricAssociation)
+        self.assertEqual(rubric_association.id, 4)
+        self.assertEqual(rubric_association.association_type, "Course")
+
+    # set_usage_rights()
+    def test_set_usage_rights(self, m):
+        register_uris({"course": ["set_usage_rights"]}, m)
+
+        usage_rights = self.course.set_usage_rights(
+            file_ids=[1, 2],
+            usage_rights={"use_justification": "fair_use", "license": "private"},
+        )
+
+        self.assertIsInstance(usage_rights, UsageRights)
+        self.assertEqual(usage_rights.use_justification, "fair_use")
+        self.assertEqual(usage_rights.message, "2 files updated")
+        self.assertEqual(usage_rights.license, "private")
+        self.assertEqual(usage_rights.file_ids, [1, 2])
+
+    # remove_usage_rights()
+    def test_remove_usage_rights(self, m):
+        register_uris({"course": ["remove_usage_rights"]}, m)
+
+        retval = self.course.remove_usage_rights(file_ids=[1, 2])
+
+        self.assertIsInstance(retval, dict)
+        self.assertIn("message", retval)
+        self.assertEqual(retval["file_ids"], [1, 2])
+        self.assertEqual(retval["message"], "2 files updated")
+
+    # get_licenses()
+    def test_get_licenses(self, m):
+        register_uris({"course": ["get_licenses"]}, m)
+
+        licenses = self.course.get_licenses()
+        self.assertIsInstance(licenses, PaginatedList)
+        licenses = list(licenses)
+
+        for l in licenses:
+            self.assertIsInstance(l, License)
+            self.assertTrue(hasattr(l, "id"))
+            self.assertTrue(hasattr(l, "name"))
+            self.assertTrue(hasattr(l, "url"))
+
+        self.assertEqual(2, len(licenses))
+
 
 @requests_mock.Mocker()
 class TestCourseNickname(unittest.TestCase):
diff --git a/tests/test_gradebook_history.py b/tests/test_gradebook_history.py
new file mode 100644
index 0000000..7a20415
--- /dev/null
+++ b/tests/test_gradebook_history.py
@@ -0,0 +1,88 @@
+from __future__ import absolute_import, division, print_function, unicode_literals
+import unittest
+
+import requests_mock
+
+from canvasapi import Canvas
+from tests import settings
+from tests.util import register_uris
+
+
+@requests_mock.Mocker()
+class TestDay(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", "get_gradebook_history_dates"]}
+            register_uris(requires, m)
+
+            self.course = self.canvas.get_course(1)
+            self.gradebook_history_dates = self.course.get_gradebook_history_dates()[0]
+
+    # __str__()
+    def test__str__(self, m):
+        string = str(self.gradebook_history_dates)
+        self.assertIsInstance(string, str)
+
+
+@requests_mock.Mocker()
+class TestGrader(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", "get_gradebook_history_details"]}
+            register_uris(requires, m)
+
+            self.course = self.canvas.get_course(1)
+            self.gradebook_history_details = self.course.get_gradebook_history_details(
+                "03-26-2019"
+            )[0]
+
+    # __str__()
+    def test__str__(self, m):
+        string = str(self.gradebook_history_details)
+        self.assertIsInstance(string, str)
+
+
+@requests_mock.Mocker()
+class TestSubmissionHistory(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", "get_submission_history"]}
+            register_uris(requires, m)
+
+            self.course = self.canvas.get_course(1)
+            self.submission_history = self.course.get_submission_history(
+                "08-23-2019", 1, 1
+            )[0]
+
+    # __str__()
+    def test__str__(self, m):
+        string = str(self.submission_history)
+        self.assertIsInstance(string, str)
+
+
+@requests_mock.Mocker()
+class TestSubmissionVersion(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", "get_uncollated_submissions"]}
+            register_uris(requires, m)
+
+            self.course = self.canvas.get_course(1)
+            self.uncollated_submissions = self.course.get_uncollated_submissions()[0]
+
+    # __str__()
+    def test__str__(self, m):
+        string = str(self.uncollated_submissions)
+        self.assertIsInstance(string, str)
diff --git a/tests/test_group.py b/tests/test_group.py
index 48c1ad3..1adb489 100644
--- a/tests/test_group.py
+++ b/tests/test_group.py
@@ -16,10 +16,12 @@ from canvasapi.exceptions import RequiredFieldMissing
 from canvasapi.external_feed import ExternalFeed
 from canvasapi.file import File
 from canvasapi.folder import Folder
+from canvasapi.license import License
 from canvasapi.paginated_list import PaginatedList
 from canvasapi.tab import Tab
 from canvasapi.content_migration import ContentMigration, Migrator
 from canvasapi.content_export import ContentExport
+from canvasapi.usage_rights import UsageRights
 from tests import settings
 from tests.util import cleanup_file, register_uris
 
@@ -620,6 +622,48 @@ class TestGroup(unittest.TestCase):
         self.assertIsInstance(content_export, ContentExport)
         self.assertTrue(hasattr(content_export, "export_type"))
 
+    # set_usage_rights()
+    def test_set_usage_rights(self, m):
+        register_uris({"group": ["set_usage_rights"]}, m)
+
+        usage_rights = self.group.set_usage_rights(
+            file_ids=[1, 2],
+            usage_rights={"use_justification": "fair_use", "license": "private"},
+        )
+
+        self.assertIsInstance(usage_rights, UsageRights)
+        self.assertEqual(usage_rights.use_justification, "fair_use")
+        self.assertEqual(usage_rights.message, "2 files updated")
+        self.assertEqual(usage_rights.license, "private")
+        self.assertEqual(usage_rights.file_ids, [1, 2])
+
+    # remove_usage_rights()
+    def test_remove_usage_rights(self, m):
+        register_uris({"group": ["remove_usage_rights"]}, m)
+
+        retval = self.group.remove_usage_rights(file_ids=[1, 2])
+
+        self.assertIsInstance(retval, dict)
+        self.assertIn("message", retval)
+        self.assertEqual(retval["file_ids"], [1, 2])
+        self.assertEqual(retval["message"], "2 files updated")
+
+    # get_licenses()
+    def test_get_licenses(self, m):
+        register_uris({"group": ["get_licenses"]}, m)
+
+        licenses = self.group.get_licenses()
+        self.assertIsInstance(licenses, PaginatedList)
+        licenses = list(licenses)
+
+        for l in licenses:
+            self.assertIsInstance(l, License)
+            self.assertTrue(hasattr(l, "id"))
+            self.assertTrue(hasattr(l, "name"))
+            self.assertTrue(hasattr(l, "url"))
+
+        self.assertEqual(2, len(licenses))
+
 
 @requests_mock.Mocker()
 class TestGroupMembership(unittest.TestCase):
diff --git a/tests/test_licenses.py b/tests/test_licenses.py
new file mode 100644
index 0000000..f5359f8
--- /dev/null
+++ b/tests/test_licenses.py
@@ -0,0 +1,27 @@
+from __future__ import absolute_import, division, print_function, unicode_literals
+import unittest
+
+import requests_mock
+
+from canvasapi import Canvas
+from tests import settings
+from tests.util import register_uris
+
+
+@requests_mock.Mocker()
+class TestLicenses(unittest.TestCase):
+    def setUp(self):
+        self.canvas = Canvas(settings.BASE_URL, settings.API_KEY)
+
+        with requests_mock.Mocker() as m:
+            requires = {"user": ["get_by_id", "get_licenses"]}
+
+            register_uris(requires, m)
+
+            self.user = self.canvas.get_user(1)
+            self.licenses = list(self.user.get_licenses())
+
+    # __str__()
+    def test__str__(self, m):
+        string = str(self.licenses[0])
+        self.assertIsInstance(string, str)
diff --git a/tests/test_login.py b/tests/test_login.py
index 2921481..d74198b 100644
--- a/tests/test_login.py
+++ b/tests/test_login.py
@@ -4,6 +4,7 @@ import unittest
 import requests_mock
 
 from canvasapi import Canvas
+from canvasapi.authentication_event import AuthenticationEvent
 from canvasapi.login import Login
 from tests import settings
 from tests.util import register_uris
@@ -49,3 +50,20 @@ class TestLogin(unittest.TestCase):
     def test__str__(self, m):
         string = str(self.login)
         self.assertIsInstance(string, str)
+
+    # get_authentication_events()
+    def test_get_authentication_events(self, m):
+        register_uris({"login": ["get_authentication_events"]}, m)
+
+        authentication_event = self.login.get_authentication_events()
+        event_list = [event for event in authentication_event]
+
+        self.assertEqual(len(event_list), 2)
+
+        self.assertIsInstance(event_list[0], AuthenticationEvent)
+        self.assertEqual(event_list[0].event_type, "login")
+        self.assertEqual(event_list[0].pseudonym_id, 9478)
+
+        self.assertIsInstance(event_list[1], AuthenticationEvent)
+        self.assertEqual(event_list[1].created_at, "2012-07-20T15:00:00-06:00")
+        self.assertEqual(event_list[1].event_type, "logout")
diff --git a/tests/test_paginated_list.py b/tests/test_paginated_list.py
index 570faa8..99fa473 100644
--- a/tests/test_paginated_list.py
+++ b/tests/test_paginated_list.py
@@ -170,3 +170,36 @@ class TestPaginatedList(unittest.TestCase):
         )
 
         self.assertIsInstance(pag_list[0], EnrollmentTerm)
+
+    def test_negative_index(self, m):
+        # Regression test for https://github.com/ucfopen/canvasapi/issues/305
+        # Ensure that we can't use negative indexing, even after loading a page
+
+        register_uris({"paginated_list": ["4_2_pages_p1", "4_2_pages_p2"]}, m)
+        pag_list = PaginatedList(User, self.requester, "GET", "four_objects_two_pages")
+        pag_list[0]
+
+        with self.assertRaises(IndexError):
+            pag_list[-1]
+
+    def test_negative_index_for_slice_start(self, m):
+        # Regression test for https://github.com/ucfopen/canvasapi/issues/305
+        # Ensure that we can't slice using a negative index as the start item
+
+        register_uris({"paginated_list": ["4_2_pages_p1", "4_2_pages_p2"]}, m)
+        pag_list = PaginatedList(User, self.requester, "GET", "four_objects_two_pages")
+        pag_list[0]
+
+        with self.assertRaises(IndexError):
+            pag_list[-1:1]
+
+    def test_negative_index_for_slice_end(self, m):
+        # Regression test for https://github.com/ucfopen/canvasapi/issues/305
+        # Ensure that we can't slice using a negative index as the end item
+
+        register_uris({"paginated_list": ["4_2_pages_p1", "4_2_pages_p2"]}, m)
+        pag_list = PaginatedList(User, self.requester, "GET", "four_objects_two_pages")
+        pag_list[0]
+
+        with self.assertRaises(IndexError):
+            pag_list[:-1]
diff --git a/tests/test_quiz.py b/tests/test_quiz.py
index 8f6fdc6..826e55e 100644
--- a/tests/test_quiz.py
+++ b/tests/test_quiz.py
@@ -13,6 +13,8 @@ from canvasapi.quiz import (
     QuizSubmissionQuestion,
     QuizQuestion,
     QuizExtension,
+    QuizSubmissionEvent,
+    QuizReport,
 )
 from canvasapi.quiz_group import QuizGroup
 from canvasapi.paginated_list import PaginatedList
@@ -36,6 +38,26 @@ class TestQuiz(unittest.TestCase):
         string = str(self.quiz)
         self.assertIsInstance(string, str)
 
+    # broadcast_message()
+    def test_broadcast_message(self, m):
+        register_uris({"quiz": ["broadcast_message"]}, m)
+
+        response = self.quiz.broadcast_message(
+            conversations={
+                "body": "please take the quiz",
+                "recipients": "submitted",
+                "subject": "ATTN: Quiz 101 Students",
+            }
+        )
+
+        self.assertTrue(response)
+
+    def test_broadcast_message_invalid_params(self, m):
+        with self.assertRaises(RequiredFieldMissing):
+            self.quiz.broadcast_message(
+                conversations={"body": "no subject here", "recipients": "submitted"}
+            )
+
     # edit()
     def test_edit(self, m):
         register_uris({"quiz": ["edit"]}, m)
@@ -203,6 +225,22 @@ class TestQuiz(unittest.TestCase):
         with self.assertRaises(RequiredFieldMissing):
             self.quiz.set_extensions([{"extra_time": 60, "extra_attempts": 3}])
 
+    # get_all_quiz_reports
+    def test_get_all_quiz_reports(self, m):
+        register_uris({"quiz": ["get_all_quiz_reports"]}, m)
+
+        reports = self.quiz.get_all_quiz_reports()
+        self.assertIsInstance(reports, PaginatedList)
+
+        reports = list(reports)
+
+        for report in reports:
+            self.assertIsInstance(report, QuizReport)
+            self.assertTrue(hasattr(report, "report_type"))
+            self.assertTrue(hasattr(report, "includes_all_versions"))
+
+        self.assertEqual(len(reports), 2)
+
     # get_all_quiz_submissions()
     def test_get_all_quiz_submissions(self, m):
         register_uris({"quiz": ["get_all_quiz_submissions"]}, m)
@@ -250,6 +288,14 @@ class TestQuiz(unittest.TestCase):
         self.assertTrue(hasattr(submission_list[1], "score"))
         self.assertEqual(submission_list[1].score, 5)
 
+    # get_quiz_report
+    def test_get_quiz_report(self, m):
+        register_uris({"quiz": ["get_quiz_report"]}, m)
+
+        report = self.quiz.get_quiz_report(1)
+        self.assertIsInstance(report, QuizReport)
+        self.assertEqual(report.quiz_id, 1)
+
     # get_quiz_submission
     def test_get_quiz_submission(self, m):
         register_uris({"quiz": ["get_quiz_submission"]}, m)
@@ -277,6 +323,58 @@ class TestQuiz(unittest.TestCase):
 
         self.assertIsInstance(submission, QuizSubmission)
 
+    def test_create_report(self, m):
+        register_uris({"quiz": ["create_report"]}, m)
+
+        report = self.quiz.create_report("student_analysis")
+
+        self.assertIsInstance(report, QuizReport)
+        self.assertEqual(report.report_type, "student_analysis")
+
+    def test_create_report_failure(self, m):
+        register_uris({"quiz": ["create_report"]}, m)
+
+        with self.assertRaises(ValueError):
+            self.quiz.create_report("super_cool_fake_report")
+
+
+@requests_mock.Mocker()
+class TestQuizReport(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"],
+                "quiz": ["get_by_id", "get_quiz_report"],
+            }
+            register_uris(requires, m)
+
+            self.course = self.canvas.get_course(1)
+            self.quiz = self.course.get_quiz(1)
+            self.quiz_report = self.quiz.get_quiz_report(1)
+
+    # __str__()
+    def test__str__(self, m):
+        string = str(self.quiz_report)
+        self.assertIsInstance(string, str)
+
+    # abort_or_delete
+    def test_abort_or_delete(self, m):
+        register_uris({"quiz": ["abort_or_delete_report"]}, m)
+
+        resp = self.quiz_report.abort_or_delete()
+
+        self.assertEqual(resp, True)
+
+    # abort_or_delete
+    def test_abort_or_delete_failure(self, m):
+        register_uris({"quiz": ["abort_or_delete_report_failure"]}, m)
+
+        resp = self.quiz_report.abort_or_delete()
+
+        self.assertEqual(resp, False)
+
 
 @requests_mock.Mocker()
 class TestQuizSubmission(unittest.TestCase):
@@ -358,6 +456,17 @@ class TestQuizSubmission(unittest.TestCase):
         self.assertTrue(hasattr(submission, "validation_token"))
         self.assertEqual(submission.score, 7)
 
+    # get_submission_events
+    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[0], QuizSubmissionEvent)
+        self.assertIsInstance(events[1], QuizSubmissionEvent)
+        self.assertEqual(str(events[0]), "page_blurred")
+        self.assertEqual(str(events[1]), "page_focused")
+
     # get_submission_questions
     def test_get_submission_questions(self, m):
         register_uris({"submission": ["get_submission_questions"]}, m)
@@ -400,6 +509,19 @@ class TestQuizSubmission(unittest.TestCase):
         with self.assertRaises(RequiredFieldMissing):
             self.submission.answer_submission_questions()
 
+    # submit_events()
+    def test_submit_events(self, m):
+        register_uris({"quiz": ["get_submission_events", "submit_events"]}, m)
+
+        test_events = self.submission.get_submission_events()
+
+        result = self.submission.submit_events(test_events)
+        self.assertTrue(result)
+
+    def test_submit_events_fail(self, m):
+        with self.assertRaises(RequiredFieldMissing):
+            self.submission.submit_events([{}])
+
 
 @requests_mock.Mocker()
 class TestQuizExtension(unittest.TestCase):
@@ -473,6 +595,26 @@ class TestQuizQuestion(unittest.TestCase):
         self.assertEqual(self.question.question_name, question_dict["question_name"])
 
 
+@requests_mock.Mocker()
+class TestQuizSubmissionEvent(unittest.TestCase):
+    def setUp(self):
+        self.canvas = Canvas(settings.BASE_URL, settings.API_KEY)
+
+        self.submission_event = QuizSubmissionEvent(
+            self.canvas._Canvas__requester,
+            {
+                "client_timestamp": "2014-10-08T19:29:58Z",
+                "event_type": "question_answered",
+                "event_data": {"answer": "42"},
+            },
+        )
+
+    # __str__()
+    def test__str__(self, m):
+        string = str(self.submission_event)
+        self.assertIsInstance(string, str)
+
+
 @requests_mock.Mocker()
 class TestQuizSubmissionQuestion(unittest.TestCase):
     def setUp(self):
diff --git a/tests/test_rubric.py b/tests/test_rubric.py
index c1e4959..d6bdae6 100644
--- a/tests/test_rubric.py
+++ b/tests/test_rubric.py
@@ -4,6 +4,7 @@ import unittest
 import requests_mock
 
 from canvasapi import Canvas
+from canvasapi.rubric import RubricAssociation
 from tests import settings
 from tests.util import register_uris
 
@@ -32,13 +33,44 @@ class TestRubricAssociation(unittest.TestCase):
 
         with requests_mock.Mocker() as m:
             register_uris(
-                {"course": ["get_by_id", "create_rubric_with_association"]}, m
+                {
+                    "course": [
+                        "get_by_id",
+                        "create_rubric_with_association",
+                        "create_rubric_association",
+                    ]
+                },
+                m,
             )
 
             self.course = self.canvas.get_course(1)
             self.rubric = self.course.create_rubric()
+            self.association = self.course.create_rubric_association()
 
     # __str__()
     def test__str__(self, m):
         string = str(self.rubric["rubric_association"])
         self.assertIsInstance(string, str)
+
+    # update
+    def test_update(self, m):
+        register_uris({"rubric": ["update_rubric_association"]}, m)
+
+        self.assertEqual(self.association.id, 4)
+
+        rubric_association = self.association.update()
+
+        self.assertEqual(rubric_association, self.association)
+        self.assertEqual(rubric_association.id, 5)
+        self.assertIsInstance(rubric_association, RubricAssociation)
+        self.assertEqual(rubric_association.association_type, "Assignment")
+
+    # delete
+    def test_delete(self, m):
+        register_uris({"rubric": ["delete_rubric_association"]}, m)
+
+        rubric_association = self.association.delete()
+
+        self.assertIsInstance(rubric_association, RubricAssociation)
+        self.assertEqual(rubric_association.id, 4)
+        self.assertEqual(rubric_association.association_type, "Assignment")
diff --git a/tests/test_submission.py b/tests/test_submission.py
index ffe0731..0bbe318 100644
--- a/tests/test_submission.py
+++ b/tests/test_submission.py
@@ -154,6 +154,29 @@ class TestGroupedSubmission(unittest.TestCase):
         self.assertIsInstance(grouped_submission.submissions, list)
         self.assertEqual(len(grouped_submission.submissions), 0)
 
+    def test__init__issue_303_regression(self):
+        """
+        Regression test for issue #303
+        https://github.com/ucfopen/canvasapi/issues/303
+        """
+        grouped_submission = GroupedSubmission(
+            self.canvas._Canvas__requester,
+            {
+                "user_id": 1,
+                "submissions": [
+                    {
+                        "id": 1,
+                        "assignments_id": 1,
+                        "user_id": 1,
+                        "html_url": "https://example.com/courses/1/assignments/1/submissions/1",
+                        "submission_type": "online_upload",
+                    }
+                ],
+            },
+        )
+        self.assertTrue(hasattr(grouped_submission, "submissions"))
+        self.assertIn("submissions", grouped_submission.attributes)
+
     # __str__()
     def test__str__(self):
         string = str(self.grouped_submission)
diff --git a/tests/test_usage_rights.py b/tests/test_usage_rights.py
new file mode 100644
index 0000000..50256e1
--- /dev/null
+++ b/tests/test_usage_rights.py
@@ -0,0 +1,29 @@
+from __future__ import absolute_import, division, print_function, unicode_literals
+import unittest
+
+import requests_mock
+
+from canvasapi import Canvas
+from tests import settings
+from tests.util import register_uris
+
+
+@requests_mock.Mocker()
+class TestUsageRights(unittest.TestCase):
+    def setUp(self):
+        self.canvas = Canvas(settings.BASE_URL, settings.API_KEY)
+
+        with requests_mock.Mocker() as m:
+            requires = {"user": ["get_by_id", "set_usage_rights"]}
+
+            register_uris(requires, m)
+
+            self.user = self.canvas.get_user(1)
+            self.usage_rights = self.user.set_usage_rights(
+                file_ids=[1, 2], usage_rights={"use_justification": "fair_use"}
+            )
+
+    # __str__()
+    def test__str__(self, m):
+        string = str(self.usage_rights)
+        self.assertIsInstance(string, str)
diff --git a/tests/test_user.py b/tests/test_user.py
index 5c0d29e..bd6b3bb 100644
--- a/tests/test_user.py
+++ b/tests/test_user.py
@@ -7,6 +7,7 @@ import requests_mock
 
 from canvasapi import Canvas
 from canvasapi.assignment import Assignment
+from canvasapi.authentication_event import AuthenticationEvent
 from canvasapi.avatar import Avatar
 from canvasapi.calendar_event import CalendarEvent
 from canvasapi.communication_channel import CommunicationChannel
@@ -18,9 +19,11 @@ from canvasapi.feature import Feature, FeatureFlag
 from canvasapi.file import File
 from canvasapi.folder import Folder
 from canvasapi.login import Login
+from canvasapi.license import License
 from canvasapi.page_view import PageView
 from canvasapi.paginated_list import PaginatedList
 from canvasapi.user import User
+from canvasapi.usage_rights import UsageRights
 from tests import settings
 from tests.util import cleanup_file, register_uris
 
@@ -563,6 +566,65 @@ class TestUser(unittest.TestCase):
         self.assertIsInstance(feature_flag, FeatureFlag)
         self.assertEqual(feature_flag.feature, "high_contrast")
 
+    # get_authentication_events()
+    def test_get_authentication_events(self, m):
+        register_uris({"user": ["get_authentication_events"]}, m)
+
+        authentication_event = self.user.get_authentication_events()
+        event_list = [event for event in authentication_event]
+
+        self.assertEqual(len(event_list), 2)
+
+        self.assertIsInstance(event_list[0], AuthenticationEvent)
+        self.assertEqual(event_list[0].event_type, "login")
+        self.assertEqual(event_list[0].pseudonym_id, 9478)
+
+        self.assertIsInstance(event_list[1], AuthenticationEvent)
+        self.assertEqual(event_list[1].created_at, "2012-07-20T15:00:00-06:00")
+        self.assertEqual(event_list[1].event_type, "logout")
+
+    # set_usage_rights()
+    def test_set_usage_rights(self, m):
+        register_uris({"user": ["set_usage_rights"]}, m)
+
+        usage_rights = self.user.set_usage_rights(
+            file_ids=[1, 2],
+            usage_rights={"use_justification": "fair_use", "license": "private"},
+        )
+
+        self.assertIsInstance(usage_rights, UsageRights)
+        self.assertEqual(usage_rights.use_justification, "fair_use")
+        self.assertEqual(usage_rights.message, "2 files updated")
+        self.assertEqual(usage_rights.license, "private")
+        self.assertEqual(usage_rights.file_ids, [1, 2])
+
+    # remove_usage_rights()
+    def test_remove_usage_rights(self, m):
+        register_uris({"user": ["remove_usage_rights"]}, m)
+
+        retval = self.user.remove_usage_rights(file_ids=[1, 2])
+
+        self.assertIsInstance(retval, dict)
+        self.assertIn("message", retval)
+        self.assertEqual(retval["file_ids"], [1, 2])
+        self.assertEqual(retval["message"], "2 files updated")
+
+    # get_licenses()
+    def test_get_licenses(self, m):
+        register_uris({"user": ["get_licenses"]}, m)
+
+        licenses = self.user.get_licenses()
+        self.assertIsInstance(licenses, PaginatedList)
+        licenses = list(licenses)
+
+        for l in licenses:
+            self.assertIsInstance(l, License)
+            self.assertTrue(hasattr(l, "id"))
+            self.assertTrue(hasattr(l, "name"))
+            self.assertTrue(hasattr(l, "url"))
+
+        self.assertEqual(2, len(licenses))
+
 
 @requests_mock.Mocker()
 class TestUserDisplay(unittest.TestCase):
diff --git a/tests/test_util.py b/tests/test_util.py
index e10972a..b530100 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -579,3 +579,9 @@ class TestUtil(unittest.TestCase):
 
         cleaned_headers = clean_headers(headers)
         self.assertIsNot(cleaned_headers, headers)
+
+    def test_clean_headers_strips_malformed_keys_correctly(self, m):
+        headers = {"Authorization": "Bearer  123,45"}
+
+        cleaned_headers = clean_headers(headers)
+        self.assertEqual(cleaned_headers["Authorization"], "****3,45")