Repository: ucfopen/canvasapi

Issue:
Grading Periods
**What resource needs additional coverage?**

Grading Periods

**What endpoints need to be covered?**

- [ ] List grading periods
- [ ] Get a single grading period
- [ ] Update a single grading period
- [ ] Delete a grading period

https://canvas.instructure.com/doc/api/grading_periods.html

Discussion:

Patch:
diff --git a/AUTHORS.md b/AUTHORS.md
index b480295..ed65e3a 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -31,6 +31,7 @@ Patches and Suggestions
 - [@jackrsteiner](https://github.com/jackrsteiner)
 - 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)
 - Mark Lalor [@MarkLalor](https://github.com/MarkLalor)
 - Markus [@elec3647](https://github.com/elec3647)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5276fe1..ed5202a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@
 
 - ContentExports (Thanks, [@weining-li](https://github.com/weining-li))
 - Favorites (Thanks, [@atarisafari](https://github.com/atarisafari))
+- Grading Periods (Thanks, [@jrsilveti](https://github.com/jrsilveti))
 - Peer Reviews (Thanks, [@vutoan1245](https://github.com/vutoan1245))
 - Planner (Thanks, [@weining-li](https://github.com/weining-li))
     - Planner
diff --git a/canvasapi/account.py b/canvasapi/account.py
index a7b9211..78ec848 100644
--- a/canvasapi/account.py
+++ b/canvasapi/account.py
@@ -7,6 +7,7 @@ from six import python_2_unicode_compatible, string_types
 from canvasapi.canvas_object import CanvasObject
 from canvasapi.exceptions import CanvasException, RequiredFieldMissing
 from canvasapi.grading_standard import GradingStandard
+from canvasapi.grading_period import GradingPeriod
 from canvasapi.paginated_list import PaginatedList
 from canvasapi.rubric import Rubric
 from canvasapi.sis_import import SisImport
@@ -1594,6 +1595,52 @@ class Account(CanvasObject):
         )
         return Admin(self._requester, response.json())
 
+    def get_grading_periods(self, **kwargs):
+        """
+        Return a list of grading periods for the associated account.
+
+        :calls: `GET /api/v1/accounts/:account_id/grading_periods \
+        <https://canvas.instructure.com/doc/api/grading_periods.html#method.grading_periods.index>`_
+
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+            :class:`canvasapi.grading_period.GradingPeriod`
+        """
+
+        return PaginatedList(
+            GradingPeriod,
+            self._requester,
+            'GET',
+            'accounts/{}/grading_periods'.format(
+                self.id
+            ),
+            {'account_id': self.id},
+            _root="grading_periods",
+            kwargs=combine_kwargs(**kwargs)
+        )
+
+    def delete_grading_period(self, grading_period):
+        """
+        Delete a grading period for an account.
+
+        :calls: `DELETE /api/v1/accounts/:account_id/grading_periods/:id \
+        <https://canvas.instructure.com/doc/api/grading_periods.html#method.grading_periods.destroy>`_
+
+        :param grading_period: The GradingPeriod object or ID to delete.
+        :type grading_period: :class:`canvasapi.grading_period.GradingPeriod` or int
+
+        :returns: True if the grading period was deleted, False otherwise.
+        :rtype: bool
+        """
+
+        grading_period_id = obj_or_id(grading_period, "grading_period", (GradingPeriod,))
+
+        response = self._requester.request(
+            'DELETE',
+            'accounts/{}/grading_periods/{}'.format(self.id, grading_period_id),
+        )
+
+        return response.json().get('delete')
+
 
 @python_2_unicode_compatible
 class AccountNotification(CanvasObject):
diff --git a/canvasapi/course.py b/canvasapi/course.py
index a14f69c..40a0815 100644
--- a/canvasapi/course.py
+++ b/canvasapi/course.py
@@ -8,6 +8,7 @@ from canvasapi.blueprint import BlueprintSubscription
 from canvasapi.canvas_object import CanvasObject
 from canvasapi.discussion_topic import DiscussionTopic
 from canvasapi.grading_standard import GradingStandard
+from canvasapi.grading_period import GradingPeriod
 from canvasapi.exceptions import RequiredFieldMissing
 from canvasapi.folder import Folder
 from canvasapi.page import Page
@@ -20,6 +21,7 @@ from canvasapi.upload import Uploader
 from canvasapi.util import combine_kwargs, is_multivalued, obj_or_id
 from canvasapi.rubric import Rubric
 
+
 warnings.simplefilter('always', DeprecationWarning)
 
 
@@ -2347,6 +2349,54 @@ class Course(CanvasObject):
             kwargs=combine_kwargs(**kwargs)
         )
 
+    def get_grading_periods(self, **kwargs):
+        """
+        Return a list of grading periods for the associated course.
+
+        :calls: `GET /api/v1/courses/:course_id/grading_periods\
+        <https://canvas.instructure.com/doc/api/grading_periods.html#method.grading_periods.index>`_
+
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+            :class:`canvasapi.grading_period.GradingPeriod`
+        """
+
+        return PaginatedList(
+            GradingPeriod,
+            self._requester,
+            'GET',
+            'courses/{}/grading_periods'.format(
+                self.id
+            ),
+            {'course_id': self.id},
+            _root="grading_periods",
+            kwargs=combine_kwargs(**kwargs)
+        )
+
+    def get_grading_period(self, grading_period, **kwargs):
+        """
+        Return a single grading period for the associated course and id.
+
+        :calls: `GET /api/v1/courses/:course_id/grading_periods/:id\
+        <https://canvas.instructure.com/doc/api/grading_periods.html#method.grading_periods.index>`_
+        :param grading_period_id: The ID of the rubric.
+        :type grading_period_id: int
+
+        :rtype: :class:`canvasapi.grading_period.GradingPeriod`
+        """
+
+        response = self._requester.request(
+            'GET',
+            'courses/{}/grading_periods/{}'.format(
+                self.id, grading_period
+            ),
+            _kwargs=combine_kwargs(**kwargs)
+        )
+
+        response_grading_period = response.json()['grading_periods'][0]
+        response_grading_period.update({'course_id': self.id})
+
+        return GradingPeriod(self._requester, response_grading_period)
+
     def get_content_exports(self, **kwargs):
         """
         Return a paginated list of the past and pending content export jobs for a course.
diff --git a/canvasapi/grading_period.py b/canvasapi/grading_period.py
new file mode 100644
index 0000000..bf91b9c
--- /dev/null
+++ b/canvasapi/grading_period.py
@@ -0,0 +1,67 @@
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+from six import python_2_unicode_compatible
+
+from canvasapi.canvas_object import CanvasObject
+from canvasapi.exceptions import RequiredFieldMissing
+from canvasapi.util import combine_kwargs
+
+
+@python_2_unicode_compatible
+class GradingPeriod(CanvasObject):
+
+    def __str__(self):
+        return '{} ({})'.format(self.title, self.id)
+
+    def update(self, grading_period, **kwargs):
+        """
+        Update a grading period for a course.
+
+        :calls: `PUT /api/v1/courses/:course_id/grading_periods/:id \
+        <https://canvas.instructure.com/doc/api/grading_periods.html#method.grading_periods.update>`_
+
+        :param grading_period: List of nested paramameters.
+        :type grading_period: list[dict]
+
+        :rtype: :class:`canvasapi.grading_period.GradingPeriod`
+        """
+        if isinstance(grading_period, list):
+            kwargs['grading_periods'] = grading_period
+        else:
+            raise RequiredFieldMissing("List is required")
+
+        if 'start_date' not in kwargs['grading_periods'][0]:
+            raise RequiredFieldMissing("start_date is missing")
+
+        if 'end_date' not in kwargs['grading_periods'][0]:
+            raise RequiredFieldMissing("end_date is missing")
+
+        response = self._requester.request(
+            'PUT',
+            'courses/{}/grading_periods/{}'.format(self.course_id, self.id),
+            _kwargs=combine_kwargs(**kwargs)
+        )
+
+        response_json = response.json()
+        grading_period = response_json['grading_periods'][0]
+        grading_period.update({'course_id': self.course_id})
+
+        return GradingPeriod(self._requester, grading_period)
+
+    def delete(self, **kwargs):
+        """
+        Delete a grading period for a course.
+
+        :calls: `DELETE /api/v1/courses/:course_id/grading_periods/:id \
+        <https://canvas.instructure.com/doc/api/grading_periods.html#method.grading_periods.update>`_
+
+        :returns: Status code 204 if delete was successful
+        :rtype: int
+        """
+        response = self._requester.request(
+            'DELETE',
+            'courses/{}/grading_periods/{}'.format(self.course_id, self.id),
+            _kwargs=combine_kwargs(**kwargs)
+        )
+
+        return response.status_code
diff --git a/canvasapi/tab.py b/canvasapi/tab.py
index 88ce8dc..7f0c7eb 100644
--- a/canvasapi/tab.py
+++ b/canvasapi/tab.py
@@ -35,6 +35,6 @@ class Tab(CanvasObject):
         response_json = response.json()
         response_json.update({'course_id': self.course_id})
 
-        super(Tab, self).set_attributes(response.json())
+        super(Tab, self).set_attributes(response_json)
 
         return self
diff --git a/docs/class-reference.rst b/docs/class-reference.rst
index 2e083d7..e2485e1 100644
--- a/docs/class-reference.rst
+++ b/docs/class-reference.rst
@@ -26,6 +26,7 @@ Class Reference
     favorite-ref
     file-ref
     folder-ref
+    grading-period-ref
     group-ref
     login-ref
     module-ref
diff --git a/docs/grading-period-ref.rst b/docs/grading-period-ref.rst
new file mode 100644
index 0000000..db57e53
--- /dev/null
+++ b/docs/grading-period-ref.rst
@@ -0,0 +1,6 @@
+=============
+GradingPeriod
+=============
+
+.. autoclass:: canvasapi.grading_period.GradingPeriod
+    :members:


Test patch:
diff --git a/tests/fixtures/account.json b/tests/fixtures/account.json
index 4d20547..669d28c 100644
--- a/tests/fixtures/account.json
+++ b/tests/fixtures/account.json
@@ -304,6 +304,23 @@
 		},
 		"status_code": 200
 	},
+	"get_grading_periods": {
+        "method": "GET",
+        "endpoint": "accounts/1/grading_periods",
+        "data": {
+            "grading_periods": [
+                {
+                    "title": "Grading period 1",
+                    "id": 1
+                },
+                {
+                    "title": "Grading period 2",
+                    "id": 2
+                }
+            ]
+        },
+        "status_code": 200
+    },
 	"get_groups_context": {
 		"method": "GET",
 		"endpoint": "accounts/1/groups",
@@ -1058,6 +1075,14 @@
 		],
 		"status_code": 200
 	},
+	"delete_grading_period": {
+        "method": "DELETE",
+        "endpoint": "accounts/1/grading_periods/1",
+        "data": {
+            "delete": true
+        },
+        "status_code": 200
+    },
 		"get_rubric_single": {
 		"method": "GET",
 		"endpoint": "accounts/1/rubrics/1",
diff --git a/tests/fixtures/course.json b/tests/fixtures/course.json
index 712f968..28c8d78 100644
--- a/tests/fixtures/course.json
+++ b/tests/fixtures/course.json
@@ -136,21 +136,21 @@
 			"id": 1,
 			"course_id": 1,
 			"associated_course_count": 3,
-            "latest_migration": null
+			"latest_migration": null
 		},
 		"status_code": 200
 	},
-    "get_blueprint_default": {
-        "method": "GET",
+	"get_blueprint_default": {
+		"method": "GET",
 		"endpoint": "courses/1/blueprint_templates/default",
 		"data": {
 			"id": 1,
 			"course_id": 1,
 			"associated_course_count": 3,
-            "latest_migration": null
+			"latest_migration": null
 		},
 		"status_code": 200
-    },
+	},
 	"get_by_id": {
 		"method": "GET",
 		"endpoint": "courses/1",
@@ -361,16 +361,16 @@
 		"method": "GET",
 		"endpoint": "courses/1/sections",
 		"data": [
-            {
-                "id": 1,
-                "name": "Section 1",
-                "course_id": 1
-            },
-            {
-                "id": 2,
-                "name": "Section 2",
-                "course_id": 1
-            }
+			{
+				"id": 1,
+				"name": "Section 1",
+				"course_id": 1
+			},
+			{
+				"id": 2,
+				"name": "Section 2",
+				"course_id": 1
+			}
 		],
 		"headers": {
 			"Link": "<https://example.com/api/v1/courses/1/sections?page=2&per_page=2>; rel=\"next\""
@@ -381,16 +381,16 @@
 		"method": "GET",
 		"endpoint": "courses/1/sections?page=2&per_page=2",
 		"data": [
-            {
-                "id": 3,
-                "name": "Section 3",
-                "course_id": 1
-            },
-            {
-                "id": 4,
-                "name": "Section 4",
-                "course_id": 1
-            }
+			{
+				"id": 3,
+				"name": "Section 3",
+				"course_id": 1
+			},
+			{
+				"id": 4,
+				"name": "Section 4",
+				"course_id": 1
+			}
 		],
 		"status_code": 200
 	},
@@ -446,23 +446,23 @@
 		],
 		"status_code": 200
 	},
-    "list_blueprint_subscriptions": {
-        "method": "GET",
-        "endpoint": "courses/1/blueprint_subscriptions",
-        "data": [
-            {
-                "id": 10,
-                "template_id": 2,
-                "blueprint_course": {
-                    "id": 1,
-                    "name": "Test Course",
-                    "course_code": "TES1337",
-                    "term_name": "Summer 2018"
-                }
-            }
-        ],
-        "status_code": 200
-    },
+	"list_blueprint_subscriptions": {
+		"method": "GET",
+		"endpoint": "courses/1/blueprint_subscriptions",
+		"data": [
+			{
+				"id": 10,
+				"template_id": 2,
+				"blueprint_course": {
+					"id": 1,
+					"name": "Test Course",
+					"course_code": "TES1337",
+					"term_name": "Summer 2018"
+				}
+			}
+		],
+		"status_code": 200
+	},
 	"list_enrollments": {
 		"method": "GET",
 		"endpoint": "courses/1/enrollments",
@@ -1399,6 +1399,39 @@
 		  },
 		"status_code": 200
 	},
+	"get_grading_periods": {
+		"method": "GET",
+		"endpoint": "courses/1/grading_periods",
+		"data": {
+			"grading_periods": [
+				{
+					"title": "Grading period 1",
+					"id": 1,
+					"course_id": 1
+				},
+				{
+					"title": "Grading period 2",
+					"id": 2,
+					"course_id": 1
+				}
+			]
+		},
+		"status_code": 200
+	},
+	"get_grading_period": {
+		"method": "GET",
+		"endpoint": "courses/1/grading_periods/1",
+		"data": {
+			"grading_periods": [
+				{
+					"title": "Grading period 1",
+					"id": 1,
+					"course_id": 1
+				}
+			]
+		},
+		"status_code": 200
+	},
 	"get_grading_standards": {
 		"method": "GET",
 		"endpoint": "courses/1/grading_standards",
diff --git a/tests/fixtures/grading_period.json b/tests/fixtures/grading_period.json
new file mode 100644
index 0000000..d1913ae
--- /dev/null
+++ b/tests/fixtures/grading_period.json
@@ -0,0 +1,24 @@
+{
+	"update": {
+		"method": "PUT",
+		"endpoint": "courses/1/grading_periods/1",
+		"data": {
+			"grading_periods": [
+                {
+                    "title": "Grading period 1",
+                    "id": 1,
+                    "start_date" : "2019-05-23T06:00:00Z",
+                    "end_date" : "2019-08-23T06:00:00Z",
+                    "weight" : 1,
+                    "course_id": 1
+                }
+            ]
+		},
+		"status_code": 200
+	},
+	"delete": {
+		"method": "DELETE",
+		"endpoint": "courses/1/grading_periods/1",
+		"status_code": 204
+	}
+}
diff --git a/tests/test_account.py b/tests/test_account.py
index 92ba871..43a7f26 100644
--- a/tests/test_account.py
+++ b/tests/test_account.py
@@ -14,10 +14,12 @@ from canvasapi.enrollment import Enrollment
 from canvasapi.enrollment_term import EnrollmentTerm
 from canvasapi.external_tool import ExternalTool
 from canvasapi.exceptions import CanvasException, RequiredFieldMissing
+from canvasapi.grading_period import GradingPeriod
 from canvasapi.grading_standard import GradingStandard
 from canvasapi.group import Group, GroupCategory
 from canvasapi.login import Login
 from canvasapi.outcome import OutcomeGroup, OutcomeLink
+from canvasapi.paginated_list import PaginatedList
 from canvasapi.rubric import Rubric
 from canvasapi.sis_import import SisImport
 from canvasapi.user import User
@@ -1053,3 +1055,28 @@ class TestAccount(unittest.TestCase):
         self.assertEqual(admin.user['login_id'], 'jdoe')
         self.assertEqual(admin.role, 'AccountAdmin')
         self.assertEqual(admin.role_id, 1)
+
+    # get_grading_periods()
+    def test_get_grading_periods(self, m):
+        register_uris({'account': ['get_grading_periods']}, m)
+
+        response = self.account.get_grading_periods()
+
+        self.assertIsInstance(response, PaginatedList)
+        self.assertIsInstance(response[0], GradingPeriod)
+        self.assertIsInstance(response[1], GradingPeriod)
+        self.assertEqual(response[0].id, 1)
+        self.assertEqual(response[1].id, 2)
+        self.assertEqual(response[0].title, "Grading period 1")
+        self.assertEqual(response[1].title, "Grading period 2")
+
+    # delete_grading_periods()
+    def test_delete_grading_periods(self, m):
+        register_uris({'account': ['delete_grading_period']}, m)
+
+        self.grading_period = GradingPeriod(
+            self.canvas._Canvas__requester,
+            {"title": "grading period 1", "id": 1, "course_id": 1}
+        )
+        self.assertTrue(self.account.delete_grading_period(1))
+        self.assertTrue(self.account.delete_grading_period(self.grading_period))
diff --git a/tests/test_course.py b/tests/test_course.py
index 762d8a2..d3ab531 100644
--- a/tests/test_course.py
+++ b/tests/test_course.py
@@ -21,6 +21,7 @@ from canvasapi.external_feed import ExternalFeed
 from canvasapi.external_tool import ExternalTool
 from canvasapi.file import File
 from canvasapi.folder import Folder
+from canvasapi.grading_period import GradingPeriod
 from canvasapi.group import Group, GroupCategory
 from canvasapi.module import Module
 from canvasapi.outcome import OutcomeGroup, OutcomeLink
@@ -1542,6 +1543,31 @@ class TestCourse(unittest.TestCase):
         self.assertEqual(blueprint_subscriptions[0].template_id, 2)
         self.assertEqual(blueprint_subscriptions[0].blueprint_course.get("id"), 1)
 
+    # list_grading_periods()
+    def test_get_grading_periods(self, m):
+        register_uris({'course': ['get_grading_periods']}, m)
+
+        response = self.course.get_grading_periods()
+
+        self.assertIsInstance(response, PaginatedList)
+        self.assertIsInstance(response[0], GradingPeriod)
+        self.assertIsInstance(response[1], GradingPeriod)
+        self.assertEqual(response[0].id, 1)
+        self.assertEqual(response[1].id, 2)
+        self.assertEqual(response[0].title, "Grading period 1")
+        self.assertEqual(response[1].title, "Grading period 2")
+
+    # get_grading_period()
+    def test_get_grading_period(self, m):
+        register_uris({'course': ['get_grading_period']}, m)
+
+        grading_period_id = 1
+        response = self.course.get_grading_period(grading_period_id)
+
+        self.assertIsInstance(response, GradingPeriod)
+        self.assertEqual(response.id, grading_period_id)
+        self.assertEqual(response.title, "Grading period 1")
+
     # get_content_exports()
     def test_list_content_exports(self, m):
         register_uris({'course': ['multiple_content_exports']}, m)
diff --git a/tests/test_grading_period.py b/tests/test_grading_period.py
new file mode 100644
index 0000000..357dce6
--- /dev/null
+++ b/tests/test_grading_period.py
@@ -0,0 +1,83 @@
+from __future__ import absolute_import, division, print_function, unicode_literals
+import unittest
+
+import requests_mock
+
+from canvasapi import Canvas
+from canvasapi.exceptions import RequiredFieldMissing
+from canvasapi.grading_period import GradingPeriod
+from tests.util import register_uris
+from tests import settings
+
+
+@requests_mock.Mocker()
+class TestGradingPeriod(unittest.TestCase):
+
+    def setUp(self):
+        self.canvas = Canvas(settings.BASE_URL, settings.API_KEY)
+
+        self.grading_period = GradingPeriod(
+            self.canvas._Canvas__requester,
+            {"title": "grading period 1", "id": 1, "course_id": 1}
+        )
+
+    def test_str(self, m):
+
+        test_str = str(self.grading_period)
+        self.assertIsInstance(test_str, str)
+
+    # update()
+    def test_update(self, m):
+        register_uris({'grading_period': ['update']}, m)
+
+        edited_grading_period = self.grading_period.update(
+            grading_period=[{
+                'start_date': '2019-06-10T06:00:00Z',
+                'end_date': '2019-06-15T06:00:00Z'
+            }]
+        )
+
+        self.assertIsInstance(edited_grading_period, GradingPeriod)
+        self.assertTrue(hasattr(edited_grading_period, 'title'))
+        self.assertTrue(hasattr(edited_grading_period, 'course_id'))
+        self.assertEqual(edited_grading_period.title, 'Grading period 1')
+        self.assertEqual(edited_grading_period.course_id, 1)
+        self.assertTrue(hasattr(edited_grading_period, 'start_date'))
+        self.assertTrue(hasattr(edited_grading_period, 'end_date'))
+        self.assertEqual(edited_grading_period.start_date, '2019-05-23T06:00:00Z')
+        self.assertEqual(edited_grading_period.end_date, '2019-08-23T06:00:00Z')
+
+    # Check that the appropriate exception is raised when no list is given.
+    def test_update_without_list(self, m):
+        register_uris({'grading_period': ['update']}, m)
+
+        with self.assertRaises(RequiredFieldMissing):
+            self.grading_period.update(
+                grading_period={
+                    'start_date': '2019-06-10T06:00:00Z',
+                    'end_date': '2019-06-15T06:00:00Z'
+                }
+            )
+
+    # Check that the grading_period that is passed has a start date
+    def test_update_without_start_date(self, m):
+        register_uris({'grading_period': ['update']}, m)
+
+        with self.assertRaises(RequiredFieldMissing):
+            self.grading_period.update(grading_period=[{
+                'end_date': '2019-06-15T06:00:00Z'
+            }])
+
+    # Check that the appropriate exception is raised when no list is given.
+    def test_update_without_end_date(self, m):
+        register_uris({'grading_period': ['update']}, m)
+
+        with self.assertRaises(RequiredFieldMissing):
+            self.grading_period.update(grading_period=[{
+                'start_date': '2019-06-10T06:00:00Z'
+            }])
+
+    # delete()
+    def test_delete(self, m):
+        register_uris({'grading_period': ['delete']}, m)
+        self.assertEqual(self.grading_period.delete(), 204)