Repository: ucfopen/canvasapi

Issue:
Outcome Import
**What resource needs additional coverage?**
Outcome Import

**What endpoints need to be covered?**

https://canvas.instructure.com/doc/api/outcome_imports.html

- [ ] Import Outcomes
- [ ] Get Outcome import status

Discussion:

Patch:
diff --git a/canvasapi/account.py b/canvasapi/account.py
index 78ec848..25ac27e 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.outcome_import import OutcomeImport
 from canvasapi.grading_period import GradingPeriod
 from canvasapi.paginated_list import PaginatedList
 from canvasapi.rubric import Rubric
@@ -1595,6 +1596,69 @@ class Account(CanvasObject):
         )
         return Admin(self._requester, response.json())
 
+    def import_outcome(self, attachment, **kwargs):
+        """
+        Import outcome into canvas.
+
+        :calls: `POST /api/v1/accounts/:account_id/outcome_imports \
+        <https://canvas.instructure.com/doc/api/outcome_imports.html#method.outcome_imports_api.create>`_
+
+        :param attachment: A file handler or path of the file to import.
+        :type attachment: file or str
+
+        :rtype: :class:`canvasapi.outcome_import.OutcomeImport`
+        """
+
+        attachment, is_path = file_or_path(attachment)
+
+        try:
+            response = self._requester.request(
+                "POST",
+                "accounts/{}/outcome_imports".format(self.id),
+                file={"attachment": attachment},
+                _kwargs=combine_kwargs(**kwargs),
+            )
+
+            response_json = response.json()
+            response_json.update({"account_id": self.id})
+
+            return OutcomeImport(self._requester, response_json)
+        finally:
+            if is_path:
+                attachment.close()
+
+    def get_outcome_import_status(self, outcome_import, **kwargs):
+        """
+        Get the status of an already created Outcome import.
+        Pass 'latest' for the outcome import id for the latest import.
+
+        :calls: `GET /api/v1/accounts/:account_id/outcome_imports/:id \
+        <https://canvas.instructure.com/doc/api/outcome_imports.html#method.outcome_imports_api.show>`_
+
+        :param outcome_import: The outcome import object or ID to get the status of.
+        :type outcome_import: :class:`canvasapi.outcome_import.OutcomeImport`,
+            int, or string: "latest"
+
+        :rtype: :class:`canvasapi.outcome_import.OutcomeImport`
+        """
+        if outcome_import == "latest":
+            outcome_import_id = "latest"
+        else:
+            outcome_import_id = obj_or_id(
+                outcome_import, "outcome_import", (OutcomeImport,)
+            )
+
+        response = self._requester.request(
+            "GET",
+            "accounts/{}/outcome_imports/{}".format(self.id, outcome_import_id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        response_json = response.json()
+        response_json.update({"account_id": self.id})
+
+        return OutcomeImport(self._requester, response_json)
+
     def get_grading_periods(self, **kwargs):
         """
         Return a list of grading periods for the associated account.
@@ -1610,9 +1674,7 @@ class Account(CanvasObject):
             GradingPeriod,
             self._requester,
             'GET',
-            'accounts/{}/grading_periods'.format(
-                self.id
-            ),
+            'accounts/{}/grading_periods'.format(self.id),
             {'account_id': self.id},
             _root="grading_periods",
             kwargs=combine_kwargs(**kwargs)
diff --git a/canvasapi/course.py b/canvasapi/course.py
index f08b2b2..d87df1a 100644
--- a/canvasapi/course.py
+++ b/canvasapi/course.py
@@ -12,6 +12,7 @@ from canvasapi.grading_standard import GradingStandard
 from canvasapi.grading_period import GradingPeriod
 from canvasapi.exceptions import RequiredFieldMissing
 from canvasapi.folder import Folder
+from canvasapi.outcome_import import OutcomeImport
 from canvasapi.page import Page
 from canvasapi.paginated_list import PaginatedList
 from canvasapi.progress import Progress
@@ -19,7 +20,7 @@ from canvasapi.quiz import QuizExtension
 from canvasapi.tab import Tab
 from canvasapi.submission import Submission
 from canvasapi.upload import Uploader
-from canvasapi.util import combine_kwargs, is_multivalued, obj_or_id
+from canvasapi.util import combine_kwargs, is_multivalued, file_or_path, obj_or_id
 from canvasapi.rubric import Rubric
 
 
@@ -2350,6 +2351,69 @@ class Course(CanvasObject):
             kwargs=combine_kwargs(**kwargs)
         )
 
+    def import_outcome(self, attachment, **kwargs):
+        """
+        Import outcome into canvas.
+
+        :calls: `POST /api/v1/courses/:course_id/outcome_imports \
+        <https://canvas.instructure.com/doc/api/outcome_imports.html#method.outcome_imports_api.create>`_
+
+        :param attachment: A file handler or path of the file to import.
+        :type attachment: file or str
+
+        :rtype: :class:`canvasapi.outcome_import.OutcomeImport`
+        """
+
+        attachment, is_path = file_or_path(attachment)
+
+        try:
+            response = self._requester.request(
+                "POST",
+                "courses/{}/outcome_imports".format(self.id),
+                file={"attachment": attachment},
+                _kwargs=combine_kwargs(**kwargs),
+            )
+
+            response_json = response.json()
+            response_json.update({"course_id": self.id})
+
+            return OutcomeImport(self._requester, response_json)
+        finally:
+            if is_path:
+                attachment.close()
+
+    def get_outcome_import_status(self, outcome_import, **kwargs):
+        """
+        Get the status of an already created Outcome import.
+        Pass 'latest' for the outcome import id for the latest import.
+
+        :calls: `GET /api/v1/courses/:course_id/outcome_imports/:id \
+        <https://canvas.instructure.com/doc/api/outcome_imports.html#method.outcome_imports_api.show>`_
+
+        :param outcome_import: The outcome import object or ID to get the status of.
+        :type outcome_import: :class:`canvasapi.outcome_import.OutcomeImport`,
+            int, or string: "latest"
+
+        :rtype: :class:`canvasapi.outcome_import.OutcomeImport`
+        """
+        if outcome_import == "latest":
+            outcome_import_id = "latest"
+        else:
+            outcome_import_id = obj_or_id(
+                outcome_import, "outcome_import", (OutcomeImport,)
+            )
+
+        response = self._requester.request(
+            "GET",
+            "courses/{}/outcome_imports/{}".format(self.id, outcome_import_id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        response_json = response.json()
+        response_json.update({"course_id": self.id})
+
+        return OutcomeImport(self._requester, response_json)
+
     def get_epub_export(self, epub, **kwargs):
         """
         Get information about a single epub export.
diff --git a/canvasapi/outcome_import.py b/canvasapi/outcome_import.py
new file mode 100644
index 0000000..c0c85bb
--- /dev/null
+++ b/canvasapi/outcome_import.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 OutcomeImport(CanvasObject):
+    def __str__(self):
+        return "{} ({})".format(self.workflow_state, self.id)
diff --git a/docs/class-reference.rst b/docs/class-reference.rst
index 0f5b4b8..e4f6b21 100644
--- a/docs/class-reference.rst
+++ b/docs/class-reference.rst
@@ -32,6 +32,7 @@ Class Reference
     login-ref
     module-ref
     outcome-ref
+    outcome-import-ref
     page-ref
     planner-ref
     poll-ref
diff --git a/docs/outcome-import-ref.rst b/docs/outcome-import-ref.rst
new file mode 100644
index 0000000..e959f6d
--- /dev/null
+++ b/docs/outcome-import-ref.rst
@@ -0,0 +1,6 @@
+==============
+OutcomeImport
+==============
+
+.. autoclass:: canvasapi.outcome_import.OutcomeImport
+    :members:
\ No newline at end of file


Test patch:
diff --git a/tests/fixtures/account.json b/tests/fixtures/account.json
index 669d28c..46fcc32 100644
--- a/tests/fixtures/account.json
+++ b/tests/fixtures/account.json
@@ -133,6 +133,23 @@
 			}
 		]
 	},
+	"import_outcome": {
+		"method": "POST",
+		"endpoint": "accounts/1/outcome_imports",
+		"data": {
+			"id": 1,
+			"created_at": "2013-12-01T23:59:00-06:00",
+			"ended_at": "2013-12-02T00:03:21-06:00",
+			"updated_at": "2013-12-02T00:03:21-06:00",
+			"workflow_state": "succeeded",
+			"data": {
+				"import_type": "instructure_csv"
+			},
+			"progress": "100",
+			"user": null
+		},
+		"status_code": 200
+	},
 	"get_by_id": {
 		"method": "GET",
 		"endpoint": "accounts/1",
@@ -408,6 +425,40 @@
 		],
 		"status_code": 200
 	},
+	"get_outcome_import_status":{
+		"method" : "GET",
+		"endpoint" : "accounts/1/outcome_imports/1",
+		"data": {
+			"id": 1,
+			"created_at": "2013-12-01T23:59:00-06:00",
+			"ended_at": "2013-12-02T00:03:21-06:00",
+			"updated_at": "2013-12-02T00:03:21-06:00",
+			"workflow_state": "succeeded",
+			"data": {
+				"import_type": "instructure_csv"
+			},
+			"progress": "100",
+			"user": null
+		},
+		"status_code": 200
+	},
+	"get_outcome_import_status_latest":{
+		"method" : "GET",
+		"endpoint" : "accounts/1/outcome_imports/latest",
+		"data": {
+			"id": 1,
+			"created_at": "2013-12-01T23:59:00-06:00",
+			"ended_at": "2013-12-02T00:03:21-06:00",
+			"updated_at": "2013-12-02T00:03:21-06:00",
+			"workflow_state": "succeeded",
+			"data": {
+				"import_type": "instructure_csv"
+			},
+			"progress": "100",
+			"user": null
+		},
+		"status_code": 200
+	},
 	"reports": {
 		"method": "GET",
 		"endpoint": "accounts/1/reports",
diff --git a/tests/fixtures/course.json b/tests/fixtures/course.json
index ab81536..1b9b787 100644
--- a/tests/fixtures/course.json
+++ b/tests/fixtures/course.json
@@ -77,6 +77,23 @@
 		},
 		"status_code": 200
 	},
+	"import_outcome": {
+		"method": "POST",
+		"endpoint": "courses/1/outcome_imports",
+		"data": {
+			"id": 1,
+			"created_at": "2013-12-01T23:59:00-06:00",
+			"ended_at": "2013-12-02T00:03:21-06:00",
+			"updated_at": "2013-12-02T00:03:21-06:00",
+			"workflow_state": "succeeded",
+			"data": {
+				"import_type": "instructure_csv"
+			},
+			"progress": "100",
+			"user": null
+		},
+		"status_code": 200
+	},
 	"get_all_assignments": {
 		"method": "GET",
 		"endpoint": "courses/1/assignments",
@@ -331,6 +348,40 @@
 		},
 		"status_code": 200
 	},
+	"get_outcome_import_status":{
+		"method" : "GET",
+		"endpoint" : "courses/1/outcome_imports/1",
+		"data": {
+			"id": 1,
+			"created_at": "2013-12-01T23:59:00-06:00",
+			"ended_at": "2013-12-02T00:03:21-06:00",
+			"updated_at": "2013-12-02T00:03:21-06:00",
+			"workflow_state": "succeeded",
+			"data": {
+				"import_type": "instructure_csv"
+			},
+			"progress": "100",
+			"user": null
+		},
+		"status_code": 200
+	},
+	"get_outcome_import_status_latest":{
+		"method" : "GET",
+		"endpoint" : "courses/1/outcome_imports/latest",
+		"data": {
+			"id": 1,
+			"created_at": "2013-12-01T23:59:00-06:00",
+			"ended_at": "2013-12-02T00:03:21-06:00",
+			"updated_at": "2013-12-02T00:03:21-06:00",
+			"workflow_state": "succeeded",
+			"data": {
+				"import_type": "instructure_csv"
+			},
+			"progress": "100",
+			"user": null
+		},
+		"status_code": 200
+	},
 	"get_quiz": {
 		"method": "GET",
 		"endpoint": "courses/1/quizzes/1",
diff --git a/tests/fixtures/test_import_outcome.csv b/tests/fixtures/test_import_outcome.csv
new file mode 100644
index 0000000..97aa03b
--- /dev/null
+++ b/tests/fixtures/test_import_outcome.csv
@@ -0,0 +1,4 @@
+vendor_guid,object_type,title,description,display_name,calculation_method,calculation_int,workflow_state,parent_guids,ratings,,,,,,,
+a,group,Parent group,parent group description,G-1,,,active,,,,,,,,,
+b,group,Child group,child group description,G-1.1,,,active,a,,,,,,,,
+c,outcome,Learning Standard,outcome description,LS-100,decaying_average,40,active,a b,3,Excellent,2,Better,1,Good,,
diff --git a/tests/test_account.py b/tests/test_account.py
index 43a7f26..29ee357 100644
--- a/tests/test_account.py
+++ b/tests/test_account.py
@@ -19,6 +19,7 @@ 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.outcome_import import OutcomeImport
 from canvasapi.paginated_list import PaginatedList
 from canvasapi.rubric import Rubric
 from canvasapi.sis_import import SisImport
@@ -1056,6 +1057,75 @@ class TestAccount(unittest.TestCase):
         self.assertEqual(admin.role, 'AccountAdmin')
         self.assertEqual(admin.role_id, 1)
 
+    # get_outcome_import_status()
+    def test_get_outcome_import_status(self, m):
+        register_uris({"account": ["get_outcome_import_status"]}, m)
+        outcome_import = self.account.get_outcome_import_status(1)
+
+        self.assertIsInstance(outcome_import, OutcomeImport)
+        self.assertEqual(outcome_import.id, 1)
+        self.assertEqual(outcome_import.workflow_state, "succeeded")
+        self.assertEqual(outcome_import.progress, "100")
+
+    def test_get_outcome_import_status_latest(self, m):
+        register_uris({"account": ["get_outcome_import_status_latest"]}, m)
+        outcome_import = self.account.get_outcome_import_status("latest")
+
+        self.assertIsInstance(outcome_import, OutcomeImport)
+        self.assertEqual(outcome_import.id, 1)
+        self.assertEqual(outcome_import.workflow_state, "succeeded")
+        self.assertEqual(outcome_import.progress, "100")
+
+    # import_outcome()
+    def test_import_outcome_filepath(self, m):
+        import os
+
+        register_uris({"account": ["import_outcome"]}, m)
+
+        filepath = os.path.join("tests", "fixtures", "test_import_outcome.csv")
+
+        outcome_import = self.account.import_outcome(filepath)
+
+        self.assertTrue(isinstance(outcome_import, OutcomeImport))
+        self.assertTrue(hasattr(outcome_import, "account_id"))
+        self.assertTrue(hasattr(outcome_import, "data"))
+        self.assertEqual(outcome_import.id, 1)
+        self.assertEqual(outcome_import.data["import_type"], "instructure_csv")
+
+    def test_import_outcome_binary(self, m):
+        import os
+
+        register_uris({"account": ["import_outcome"]}, m)
+
+        filepath = os.path.join("tests", "fixtures", "test_import_outcome.csv")
+
+        with open(filepath, "rb") as f:
+            outcome_import = self.account.import_outcome(f)
+
+        self.assertTrue(isinstance(outcome_import, OutcomeImport))
+        self.assertTrue(hasattr(outcome_import, "account_id"))
+        self.assertTrue(hasattr(outcome_import, "data"))
+        self.assertEqual(outcome_import.id, 1)
+        self.assertEqual(outcome_import.data["import_type"], "instructure_csv")
+
+    def test_import_outcome_id(self, m):
+
+        register_uris({"account": ["import_outcome"]}, m)
+
+        outcome_import = self.account.import_outcome(1)
+
+        self.assertTrue(isinstance(outcome_import, OutcomeImport))
+        self.assertTrue(hasattr(outcome_import, "account_id"))
+        self.assertTrue(hasattr(outcome_import, "data"))
+        self.assertEqual(outcome_import.id, 1)
+        self.assertEqual(outcome_import.data["import_type"], "instructure_csv")
+
+    def test_import_outcome_ioerror(self, m):
+        f = "!@#$%^&*()_+QWERTYUIOP{}|"
+
+        with self.assertRaises(IOError):
+            self.account.import_outcome(f)
+
     # get_grading_periods()
     def test_get_grading_periods(self, m):
         register_uris({'account': ['get_grading_periods']}, m)
diff --git a/tests/test_course.py b/tests/test_course.py
index 8b1a865..70fb8df 100644
--- a/tests/test_course.py
+++ b/tests/test_course.py
@@ -26,6 +26,7 @@ from canvasapi.grading_period import GradingPeriod
 from canvasapi.group import Group, GroupCategory
 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
@@ -1544,6 +1545,75 @@ class TestCourse(unittest.TestCase):
         self.assertEqual(blueprint_subscriptions[0].template_id, 2)
         self.assertEqual(blueprint_subscriptions[0].blueprint_course.get("id"), 1)
 
+    # get_outcome_import_status()
+    def test_get_outcome_import_status(self, m):
+        register_uris({"course": ["get_outcome_import_status"]}, m)
+        outcome_import = self.course.get_outcome_import_status(1)
+
+        self.assertIsInstance(outcome_import, OutcomeImport)
+        self.assertEqual(outcome_import.id, 1)
+        self.assertEqual(outcome_import.workflow_state, "succeeded")
+        self.assertEqual(outcome_import.progress, "100")
+
+    def test_get_outcome_import_status_latest(self, m):
+        register_uris({"course": ["get_outcome_import_status_latest"]}, m)
+        outcome_import = self.course.get_outcome_import_status("latest")
+
+        self.assertIsInstance(outcome_import, OutcomeImport)
+        self.assertEqual(outcome_import.id, 1)
+        self.assertEqual(outcome_import.workflow_state, "succeeded")
+        self.assertEqual(outcome_import.progress, "100")
+
+    # import_outcome()
+    def test_import_outcome_filepath(self, m):
+        import os
+
+        register_uris({"course": ["import_outcome"]}, m)
+
+        filepath = os.path.join("tests", "fixtures", "test_import_outcome.csv")
+
+        outcome_import = self.course.import_outcome(filepath)
+
+        self.assertTrue(isinstance(outcome_import, OutcomeImport))
+        self.assertTrue(hasattr(outcome_import, "course_id"))
+        self.assertTrue(hasattr(outcome_import, "data"))
+        self.assertEqual(outcome_import.id, 1)
+        self.assertEqual(outcome_import.data["import_type"], "instructure_csv")
+
+    def test_import_outcome_binary(self, m):
+        import os
+
+        register_uris({"course": ["import_outcome"]}, m)
+
+        filepath = os.path.join("tests", "fixtures", "test_import_outcome.csv")
+
+        with open(filepath, "rb") as f:
+            outcome_import = self.course.import_outcome(f)
+
+        self.assertTrue(isinstance(outcome_import, OutcomeImport))
+        self.assertTrue(hasattr(outcome_import, "course_id"))
+        self.assertTrue(hasattr(outcome_import, "data"))
+        self.assertEqual(outcome_import.id, 1)
+        self.assertEqual(outcome_import.data["import_type"], "instructure_csv")
+
+    def test_import_outcome_id(self, m):
+
+        register_uris({"course": ["import_outcome"]}, m)
+
+        outcome_import = self.course.import_outcome(1)
+
+        self.assertTrue(isinstance(outcome_import, OutcomeImport))
+        self.assertTrue(hasattr(outcome_import, "course_id"))
+        self.assertTrue(hasattr(outcome_import, "data"))
+        self.assertEqual(outcome_import.id, 1)
+        self.assertEqual(outcome_import.data["import_type"], "instructure_csv")
+
+    def test_import_outcome_ioerror(self, m):
+        f = "!@#$%^&*()_+QWERTYUIOP{}|"
+
+        with self.assertRaises(IOError):
+            self.course.import_outcome(f)
+
     # get_epub_export
     def test_get_epub_export(self, m):
         register_uris({'course': ['get_epub_export']}, m)
diff --git a/tests/test_outcome_import.py b/tests/test_outcome_import.py
new file mode 100644
index 0000000..694baae
--- /dev/null
+++ b/tests/test_outcome_import.py
@@ -0,0 +1,23 @@
+from __future__ import absolute_import, division, print_function, unicode_literals
+import unittest
+
+import requests_mock
+
+from canvasapi import Canvas
+from canvasapi.outcome_import import OutcomeImport
+from tests import settings
+
+
+@requests_mock.Mocker()
+class TestOutcomeImport(unittest.TestCase):
+    def setUp(self):
+        self.canvas = Canvas(settings.BASE_URL, settings.API_KEY)
+
+        self.outcome_import = OutcomeImport(
+            self.canvas._Canvas__requester, {"id": 1, "workflow_state": 1}
+        )
+
+    def test_str(self, m):
+
+        test_str = str(self.outcome_import)
+        self.assertIsInstance(test_str, str)