Repository: ucfopen/canvasapi (python)

Issue:
Add endpoint to get selective_data for a content migration
**What resource needs additional coverage?**
ContentMigration

**What endpoints need to be covered?**
Endpoint name: "List items for selective import"
URLS:
* GET /api/v1/accounts/:account_id/content_migrations/:id/selective_data
* GET /api/v1/courses/:course_id/content_migrations/:id/selective_data
* GET /api/v1/groups/:group_id/content_migrations/:id/selective_data
* GET /api/v1/users/:user_id/content_migrations/:id/selective_data 

Docs: https://canvas.instructure.com/doc/api/content_migrations.html#method.content_migrations.content_list

Pull request description:
Issue #502: Add endpoint to get selective_data for a content migration
# Proposed Changes

* Add get_selective_data() to canvasapi.content_migration.ContentMIgration to return nodes available for selection when a migration is created with selective_import=true

Fixes #502 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3f6e30a..6fda3e2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
 ### New Endpoint Coverage
 
 - Delete a Rubric (Thanks, [@kailukaitisBrendan](https://github.com/kailukaitisBrendan))
+- Content Migrations: List items for selective import (Thanks, [@matthewf-ucsd](https://github.com/matthewf-ucsd))
 
 ### Breaking Changes
 
diff --git a/canvasapi/content_migration.py b/canvasapi/content_migration.py
index fa5a792..0679bdd 100644
--- a/canvasapi/content_migration.py
+++ b/canvasapi/content_migration.py
@@ -186,6 +186,48 @@ class ContentMigration(CanvasObject):
         )
         return Progress(self._requester, response.json())
 
+    def get_selective_data(self, **kwargs):
+        """
+        Return the selective data associated with this content migration. Use this to get a list
+        of available objects to import for a migration created with 'waiting_for_select=True'.
+
+        :calls:
+            `GET
+            /api/v1/accounts/:account_id/content_migrations/:content_migration_id/selective_data
+            <https://canvas.instructure.com/doc/api/content_migrations.html#method.content_migrations.content_list>`_
+
+            or `GET
+            /api/v1/courses/:course_id/content_migrations/:content_migration_id/selective_data
+            <https://canvas.instructure.com/doc/api/content_migrations.html#method.content_migrations.content_list>`_
+
+            or `GET
+            /api/v1/groups/:group_id/content_migrations/:content_migration_id/selective_data
+            <https://canvas.instructure.com/doc/api/content_migrations.html#method.content_migrations.content_list>`_
+
+            or `GET
+            /api/v1/users/:user_id/content_migrations/:content_migration_id/selective_data
+            <https://canvas.instructure.com/doc/api/content_migrations.html#method.content_migrations.content_list>`_
+
+        :returns: Paginated List of of nodes available for selection in this migration
+        :rtype: :class:`canvasapi.content_migration.ContentMigrationSelectionNode`
+        """
+        from canvasapi.content_migration import ContentMigrationSelectionNode
+
+        return PaginatedList(
+            ContentMigrationSelectionNode,
+            self._requester,
+            "GET",
+            "{}s/{}/content_migrations/{}/selective_data".format(
+                self._parent_type, self._parent_id, self.id
+            ),
+            {
+                "context_type": self._parent_type,
+                "context_id": self._parent_id,
+                "content_migration_id": self.id,
+            },
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
     def update(self, **kwargs):
         """
         Update an existing content migration.
@@ -220,6 +262,11 @@ class ContentMigration(CanvasObject):
             return False
 
 
+class ContentMigrationSelectionNode(CanvasObject):
+    def __str__(self):
+        return "{}".format(self.type)
+
+
 class MigrationIssue(CanvasObject):
     def __str__(self):
         return "{}: {}".format(self.id, self.description)
diff --git a/docs/content-migration-ref.rst b/docs/content-migration-ref.rst
index 12c47c0..9571861 100644
--- a/docs/content-migration-ref.rst
+++ b/docs/content-migration-ref.rst
@@ -5,6 +5,13 @@ ContentMigration
 .. autoclass:: canvasapi.content_migration.ContentMigration
     :members:
 
+=============================
+ContentMigrationSelectionNode
+=============================
+
+.. autoclass:: canvasapi.content_migration.ContentMigrationSelectionNode
+    :members:
+
 ==============
 MigrationIssue
 ==============


Test patch:
diff --git a/tests/fixtures/content_migration.json b/tests/fixtures/content_migration.json
index cb3b531..2dab15f 100644
--- a/tests/fixtures/content_migration.json
+++ b/tests/fixtures/content_migration.json
@@ -141,6 +141,368 @@
 		},
 		"status_code": 200
 	},
+	"get_selective_data_account": {
+		"method": "GET",
+		"endpoint": "accounts/1/content_migrations/1/selective_data",
+		"data": [
+			{
+			"content_migration_id": 1,
+			"context_id": 1,
+			"context_type": "account",
+			"property": "copy[all_account_settings]",
+			"title": "Account Settings",
+			"type": "account_settings"
+			},
+			{
+			"content_migration_id": 1,
+			"context_id": 1,
+			"context_type": "account",
+			"count": 2,
+			"property": "copy[all_assignments]",
+			"sub_items_url": "https://example.com/api/v1/accounts/1/content_migrations/1/selective_data?type=assignments",
+			"title": "Assignments",
+			"type": "assignments"
+			}
+		],
+		"status_code": 200
+	},
+	"get_selective_data_course": {
+		"method": "GET",
+		"endpoint": "courses/1/content_migrations/1/selective_data",
+		"data": [
+			{
+			"content_migration_id": 1,
+			"context_id": 1,
+			"context_type": "course",
+			"property": "copy[all_course_settings]",
+			"title": "Course Settings",
+			"type": "course_settings"
+			},
+			{
+			"content_migration_id": 1,
+			"context_id": 1,
+			"context_type": "course",
+			"count": 2,
+			"property": "copy[all_assignments]",
+			"sub_items_url": "https://example.com/api/v1/courses/1/content_migrations/1/selective_data?type=assignments",
+			"title": "Assignments",
+			"type": "assignments"
+			}
+		],
+		"status_code": 200
+	},
+	"get_selective_data_group": {
+		"method": "GET",
+		"endpoint": "groups/1/content_migrations/1/selective_data",
+		"data": [
+			{
+			"content_migration_id": 1,
+			"context_id": 1,
+			"context_type": "group",
+			"property": "copy[all_group_settings]",
+			"title": "Group Settings",
+			"type": "group_settings"
+			},
+			{
+			"content_migration_id": 1,
+			"context_id": 1,
+			"context_type": "group",
+			"count": 2,
+			"property": "copy[all_assignments]",
+			"sub_items_url": "https://example.com/api/v1/groups/1/content_migrations/1/selective_data?type=assignments",
+			"title": "Assignments",
+			"type": "assignments"
+			}
+		],
+		"status_code": 200
+	},
+	"get_selective_data_user": {
+		"method": "GET",
+		"endpoint": "users/1/content_migrations/1/selective_data",
+		"data": [
+			{
+			"content_migration_id": 1,
+			"context_id": 1,
+			"context_type": "user",
+			"property": "copy[all_user_settings]",
+			"title": "user Settings",
+			"type": "user_settings"
+			},
+			{
+			"content_migration_id": 1,
+			"context_id": 1,
+			"context_type": "user",
+			"count": 2,
+			"property": "copy[all_assignments]",
+			"sub_items_url": "https://example.com/api/v1/users/1/content_migrations/1/selective_data?type=assignments",
+			"title": "Assignments",
+			"type": "assignments"
+			}
+		],
+		"status_code": 200
+	},
+	"get_selective_data_assignments_account": {
+		"method": "GET",
+		"endpoint": "accounts/1/content_migrations/1/selective_data?type=assignments",
+		"data": [
+			{
+				"content_migration_id": 1,
+				"context_id": 1,
+				"context_type": "account",
+				"migration_id": "g8b133a71f8693ea12f549743bf59d194",
+				"property": "copy[assignment_groups][id_ga1f85a92f38ea154ea78bdf34af5a945]",
+				"sub_items": [
+				{
+					"linked_resource": {
+					"message": "linked with Discussion \"Topic Week 1: Discussion\"",
+					"migration_id": "ge1a27af717d06a0782d708c9441be1f68",
+					"property": "copy[discussion_topics][id_g1e4eabe30567ae83bfa62d08b88e56f08]",
+					"type": "discussion_topics"
+					},
+					"migration_id": "g486ff8a1e91a1d9403125c838ea2a75c8",
+					"property": "copy[assignments][id_g4c51f316971e47ac786d5cc1b16d2b198]",
+					"title": "Week 1: Discussion",
+					"type": "assignments"
+				},
+				{
+					"linked_resource": {
+					"message": "linked with Discussion Topic \"Week 2: Discussion\"",
+					"migration_id": "g047d0ed5637242f5623cb932e347ec3c5",
+					"property": "copy[discussion_topics][id_ge517692a894ba8a3e95a2a18655e13905]",
+					"type": "discussion_topics"
+					},
+					"migration_id": "g0d58cddab9615f2226a44c0aa44bf33eb",
+					"property": "copy[assignments][id_g2777dc21adf38a770697313c7ca693e8b]",
+					"title": "Week 2: Discussion",
+					"type": "assignments"
+				}
+				],
+				"title": "Discussion",
+				"type": "assignment_groups"
+			},
+			{
+				"content_migration_id": 1,
+				"context_id": 1,
+				"context_type": "account",
+				"migration_id": "gaba50c7f7eb473769f7dfb649f8ae6bac",
+				"property": "copy[assignment_groups][id_ge9c31bec83eeeff8c33e539293b824c5c]",
+				"sub_items": [
+				{
+					"migration_id": "g2d182762c23c79eec72fdbfaf4e72f497",
+					"property": "copy[assignments][id_g7510f7e9257131eedbf456fa851ce2727]",
+					"title": "Week 1: Assignment",
+					"type": "assignments"
+				},
+				{
+					"migration_id": "g0d4b9570357bf08230b2b5def78513da0",
+					"property": "copy[assignments][id_g8ed8a7082b0884b2ed9d1adcb2b0610e0]",
+					"title": "Week 2: Assignment",
+					"type": "assignments"
+				}
+				],
+				"title": "Assignments",
+				"type": "assignment_groups"
+			}
+			],
+		"status_code": 200
+	},
+
+	"get_selective_data_assignments_course": {
+		"method": "GET",
+		"endpoint": "courses/1/content_migrations/1/selective_data?type=assignments",
+		"data": [
+			{
+				"content_migration_id": 1,
+				"context_id": 1,
+				"context_type": "course",
+				"migration_id": "g8b133a71f8693ea12f549743bf59d194",
+				"property": "copy[assignment_groups][id_ga1f85a92f38ea154ea78bdf34af5a945]",
+				"sub_items": [
+				{
+					"linked_resource": {
+					"message": "linked with Discussion \"Topic Week 1: Discussion\"",
+					"migration_id": "ge1a27af717d06a0782d708c9441be1f68",
+					"property": "copy[discussion_topics][id_g1e4eabe30567ae83bfa62d08b88e56f08]",
+					"type": "discussion_topics"
+					},
+					"migration_id": "g486ff8a1e91a1d9403125c838ea2a75c8",
+					"property": "copy[assignments][id_g4c51f316971e47ac786d5cc1b16d2b198]",
+					"title": "Week 1: Discussion",
+					"type": "assignments"
+				},
+				{
+					"linked_resource": {
+					"message": "linked with Discussion Topic \"Week 2: Discussion\"",
+					"migration_id": "g047d0ed5637242f5623cb932e347ec3c5",
+					"property": "copy[discussion_topics][id_ge517692a894ba8a3e95a2a18655e13905]",
+					"type": "discussion_topics"
+					},
+					"migration_id": "g0d58cddab9615f2226a44c0aa44bf33eb",
+					"property": "copy[assignments][id_g2777dc21adf38a770697313c7ca693e8b]",
+					"title": "Week 2: Discussion",
+					"type": "assignments"
+				}
+				],
+				"title": "Discussion",
+				"type": "assignment_groups"
+			},
+			{
+				"content_migration_id": 1,
+				"context_id": 1,
+				"context_type": "course",
+				"migration_id": "gaba50c7f7eb473769f7dfb649f8ae6bac",
+				"property": "copy[assignment_groups][id_ge9c31bec83eeeff8c33e539293b824c5c]",
+				"sub_items": [
+				{
+					"migration_id": "g2d182762c23c79eec72fdbfaf4e72f497",
+					"property": "copy[assignments][id_g7510f7e9257131eedbf456fa851ce2727]",
+					"title": "Week 1: Assignment",
+					"type": "assignments"
+				},
+				{
+					"migration_id": "g0d4b9570357bf08230b2b5def78513da0",
+					"property": "copy[assignments][id_g8ed8a7082b0884b2ed9d1adcb2b0610e0]",
+					"title": "Week 2: Assignment",
+					"type": "assignments"
+				}
+				],
+				"title": "Assignments",
+				"type": "assignment_groups"
+			}
+			],
+		"status_code": 200
+	},
+	"get_selective_data_assignments_user": {
+		"method": "GET",
+		"endpoint": "users/1/content_migrations/1/selective_data?type=assignments",
+		"data": [
+			{
+				"content_migration_id": 1,
+				"context_id": 1,
+				"context_type": "user",
+				"migration_id": "g8b133a71f8693ea12f549743bf59d194",
+				"property": "copy[assignment_groups][id_ga1f85a92f38ea154ea78bdf34af5a945]",
+				"sub_items": [
+				{
+					"linked_resource": {
+					"message": "linked with Discussion \"Topic Week 1: Discussion\"",
+					"migration_id": "ge1a27af717d06a0782d708c9441be1f68",
+					"property": "copy[discussion_topics][id_g1e4eabe30567ae83bfa62d08b88e56f08]",
+					"type": "discussion_topics"
+					},
+					"migration_id": "g486ff8a1e91a1d9403125c838ea2a75c8",
+					"property": "copy[assignments][id_g4c51f316971e47ac786d5cc1b16d2b198]",
+					"title": "Week 1: Discussion",
+					"type": "assignments"
+				},
+				{
+					"linked_resource": {
+					"message": "linked with Discussion Topic \"Week 2: Discussion\"",
+					"migration_id": "g047d0ed5637242f5623cb932e347ec3c5",
+					"property": "copy[discussion_topics][id_ge517692a894ba8a3e95a2a18655e13905]",
+					"type": "discussion_topics"
+					},
+					"migration_id": "g0d58cddab9615f2226a44c0aa44bf33eb",
+					"property": "copy[assignments][id_g2777dc21adf38a770697313c7ca693e8b]",
+					"title": "Week 2: Discussion",
+					"type": "assignments"
+				}
+				],
+				"title": "Discussion",
+				"type": "assignment_groups"
+			},
+			{
+				"content_migration_id": 1,
+				"context_id": 1,
+				"context_type": "user",
+				"migration_id": "gaba50c7f7eb473769f7dfb649f8ae6bac",
+				"property": "copy[assignment_groups][id_ge9c31bec83eeeff8c33e539293b824c5c]",
+				"sub_items": [
+				{
+					"migration_id": "g2d182762c23c79eec72fdbfaf4e72f497",
+					"property": "copy[assignments][id_g7510f7e9257131eedbf456fa851ce2727]",
+					"title": "Week 1: Assignment",
+					"type": "assignments"
+				},
+				{
+					"migration_id": "g0d4b9570357bf08230b2b5def78513da0",
+					"property": "copy[assignments][id_g8ed8a7082b0884b2ed9d1adcb2b0610e0]",
+					"title": "Week 2: Assignment",
+					"type": "assignments"
+				}
+				],
+				"title": "Assignments",
+				"type": "assignment_groups"
+			}
+			],
+		"status_code": 200
+	},
+
+	"get_selective_data_assignments_user": {
+		"method": "GET",
+		"endpoint": "users/1/content_migrations/1/selective_data?type=assignments",
+		"data": [
+			{
+				"content_migration_id": 1,
+				"context_id": 1,
+				"context_type": "user",
+				"migration_id": "g8b133a71f8693ea12f549743bf59d194",
+				"property": "copy[assignment_groups][id_ga1f85a92f38ea154ea78bdf34af5a945]",
+				"sub_items": [
+				{
+					"linked_resource": {
+					"message": "linked with Discussion \"Topic Week 1: Discussion\"",
+					"migration_id": "ge1a27af717d06a0782d708c9441be1f68",
+					"property": "copy[discussion_topics][id_g1e4eabe30567ae83bfa62d08b88e56f08]",
+					"type": "discussion_topics"
+					},
+					"migration_id": "g486ff8a1e91a1d9403125c838ea2a75c8",
+					"property": "copy[assignments][id_g4c51f316971e47ac786d5cc1b16d2b198]",
+					"title": "Week 1: Discussion",
+					"type": "assignments"
+				},
+				{
+					"linked_resource": {
+					"message": "linked with Discussion Topic \"Week 2: Discussion\"",
+					"migration_id": "g047d0ed5637242f5623cb932e347ec3c5",
+					"property": "copy[discussion_topics][id_ge517692a894ba8a3e95a2a18655e13905]",
+					"type": "discussion_topics"
+					},
+					"migration_id": "g0d58cddab9615f2226a44c0aa44bf33eb",
+					"property": "copy[assignments][id_g2777dc21adf38a770697313c7ca693e8b]",
+					"title": "Week 2: Discussion",
+					"type": "assignments"
+				}
+				],
+				"title": "Discussion",
+				"type": "assignment_groups"
+			},
+			{
+				"content_migration_id": 1,
+				"context_id": 1,
+				"context_type": "user",
+				"migration_id": "gaba50c7f7eb473769f7dfb649f8ae6bac",
+				"property": "copy[assignment_groups][id_ge9c31bec83eeeff8c33e539293b824c5c]",
+				"sub_items": [
+				{
+					"migration_id": "g2d182762c23c79eec72fdbfaf4e72f497",
+					"property": "copy[assignments][id_g7510f7e9257131eedbf456fa851ce2727]",
+					"title": "Week 1: Assignment",
+					"type": "assignments"
+				},
+				{
+					"migration_id": "g0d4b9570357bf08230b2b5def78513da0",
+					"property": "copy[assignments][id_g8ed8a7082b0884b2ed9d1adcb2b0610e0]",
+					"title": "Week 2: Assignment",
+					"type": "assignments"
+				}
+				],
+				"title": "Assignments",
+				"type": "assignment_groups"
+			}
+			],
+		"status_code": 200
+	},
 	"update_issue": {
 		"method": "PUT",
 		"endpoint": "accounts/1/content_migrations/1/migration_issues/1",
diff --git a/tests/test_content_migration.py b/tests/test_content_migration.py
index 553d0ae..bada70e 100644
--- a/tests/test_content_migration.py
+++ b/tests/test_content_migration.py
@@ -4,7 +4,11 @@ import requests_mock
 
 from canvasapi import Canvas
 from canvasapi.account import Account
-from canvasapi.content_migration import ContentMigration, MigrationIssue
+from canvasapi.content_migration import (
+    ContentMigration,
+    ContentMigrationSelectionNode,
+    MigrationIssue,
+)
 from canvasapi.course import Course
 from canvasapi.group import Group
 from canvasapi.progress import Progress
@@ -256,3 +260,50 @@ class TestMigrator(unittest.TestCase):
     def test__str__(self, m):
         string = str(self.migrator)
         self.assertIsInstance(string, str)
+
+
+@requests_mock.Mocker()
+class TestSelectiveData(unittest.TestCase):
+    def setUp(self):
+        self.canvas = Canvas(settings.BASE_URL, settings.API_KEY)
+
+        with requests_mock.Mocker() as m:
+            requires = {
+                "course": ["get_by_id"],
+                "group": ["get_by_id"],
+                "account": ["get_by_id"],
+                "user": ["get_by_id"],
+            }
+            register_uris(requires, m)
+
+            self.account = self.canvas.get_account(1)
+            self.course = self.canvas.get_course(1)
+            self.group = self.canvas.get_group(1)
+            self.user = self.canvas.get_user(1)
+
+    def test_get_selective_data(self, m):
+        register_uris(
+            {
+                "course": ["get_content_migration_single"],
+                "group": ["get_content_migration_single"],
+                "account": ["get_content_migration_single"],
+                "user": ["get_content_migration_single"],
+                "content_migration": [
+                    "get_selective_data_account",
+                    "get_selective_data_course",
+                    "get_selective_data_group",
+                    "get_selective_data_user",
+                ],
+            },
+            m,
+        )
+        for context_type in ["account", "course", "group", "user"]:
+            context = getattr(self, context_type)
+            migration = context.get_content_migration(1)
+            for node in migration.get_selective_data():
+                self.assertIsInstance(node, ContentMigrationSelectionNode)
+                self.assertIsInstance(str(node), str)
+
+            for node in migration.get_selective_data(type="assignments"):
+                self.assertIsInstance(node, ContentMigrationSelectionNode)
+                self.assertIsInstance(str(node), str)