From 340879fcf9cefd75fa785b9c56fdf94451883bfa Mon Sep 17 00:00:00 2001 From: Brian Date: Tue, 9 Aug 2022 14:54:40 -0400 Subject: [PATCH] Issue/525 eportfolio endpoint (#536) * Create new classes, add user methods Create the `EPortfolio` and `EPortfolioPage` classes to match Canvas docs. Add relevant ePortfolio methods to `User` class. * Remove ID arg from all methods The module is to work with single eportfolios already retrieved through the `Canvas` object. The IDs were redundant and were removed. * EPortfolio module tests Added fixutres for `EPortfolio` module. All tests passing. * Add `get_eportfolio` method The ePortfolio endpoint isn't linked to a specific course, so adding it to the Canvas module made sense. There is another route for users to retrieve their own ePortfolios in a `PaginatedList`. Test added and passing. * fix accidental formatting of json file * Add string method tests All tests passing. * fix flake8, isort errors * fix isort error * Docstring fixes. Add eportfolio ref * reformat eport json * Fix markdown typo Co-authored-by: Matthew Emond --- AUTHORS.md | 1 + CHANGELOG.md | 1 + canvasapi/canvas.py | 22 +++++++++ canvasapi/eportfolio.py | 86 ++++++++++++++++++++++++++++++++++ canvasapi/user.py | 50 ++++++++++++++++++-- docs/class-reference.rst | 1 + docs/eportfolio-ref.rst | 13 +++++ tests/fixtures/eportfolio.json | 75 +++++++++++++++++++++++++++++ tests/fixtures/user.json | 56 ++++++++++++++++++++++ tests/test_canvas.py | 10 ++++ tests/test_eportfolio.py | 73 +++++++++++++++++++++++++++++ tests/test_user.py | 35 ++++++++++++++ 12 files changed, 420 insertions(+), 3 deletions(-) create mode 100644 canvasapi/eportfolio.py create mode 100644 docs/eportfolio-ref.rst create mode 100644 tests/fixtures/eportfolio.json create mode 100644 tests/test_eportfolio.py diff --git a/AUTHORS.md b/AUTHORS.md index eaf346e9..e25c95ed 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -20,6 +20,7 @@ Patches and Suggestions - Ashutosh Saxena [@Xx-Ashutosh-xX](https://github.com/Xx-Ashutosh-xX) - Ben Liblit [@liblit](https://github.com/liblit) - Bill Wrbican [@wjw27](https://github.com/wjw27) +- [@Birdmaaan4](https://github.com/Birdmaaan4) - [@blepabyte](https://github.com/blepabyte) - Bradford Lynch [@bradfordlynch](https://github.com/bradfordlynch) - Brian Bennett [@bennettscience](https://github.com/bennettscience) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76b1945f..a53821fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Grade Change Log for Assignments, Courses, and Users (Thanks, [@matthewf-ucsd](https://github.com/matthewf-ucsd)) - Content Migrations: List items for selective import (Thanks, [@matthewf-ucsd](https://github.com/matthewf-ucsd)) - List observers of a User (Thanks, [@bennettscience](https://github.com/bennettscience)) +- ePortfolio endpoints (Thanks, [@Birdmaaan4](https://github.com/Birdmaaan4) and [@bennettscience](https://github.com/bennettscience)) ### General diff --git a/canvasapi/canvas.py b/canvasapi/canvas.py index 51b632ca..ede61940 100644 --- a/canvasapi/canvas.py +++ b/canvasapi/canvas.py @@ -5,6 +5,7 @@ from canvasapi.course import Course from canvasapi.course_epub_export import CourseEpubExport from canvasapi.current_user import CurrentUser +from canvasapi.eportfolio import EPortfolio from canvasapi.exceptions import RequiredFieldMissing from canvasapi.file import File from canvasapi.folder import Folder @@ -771,6 +772,27 @@ def get_current_user(self): """ return CurrentUser(self.__requester) + def get_eportfolio(self, eportfolio, **kwargs): + """ + Get an eportfolio by ID. + + :param eportfolio: The object or ID of the eportfolio to retrieve. + :type eportfolio: :class: `canvasapi.eportfolio.EPortfolio` or int + + :calls: `GET /api/v1/eportfolios/:id` \ + ``_ + + :rtype: :class:`canvasapi.eportfolio.EPortfolio` + """ + eportfolio_id = obj_or_id(eportfolio, "eportfolio", (EPortfolio,)) + response = self.__requester.request( + "GET", + "eportfolios/{}".format(eportfolio_id), + _kwargs=combine_kwargs(**kwargs), + ) + + return EPortfolio(self.__requester, response.json()) + def get_epub_exports(self, **kwargs): """ Return a list of epub exports for the associated course. diff --git a/canvasapi/eportfolio.py b/canvasapi/eportfolio.py new file mode 100644 index 00000000..f0eecb86 --- /dev/null +++ b/canvasapi/eportfolio.py @@ -0,0 +1,86 @@ +from canvasapi.canvas_object import CanvasObject +from canvasapi.paginated_list import PaginatedList +from canvasapi.util import combine_kwargs + + +class EPortfolio(CanvasObject): + def __str__(self): + return "{}".format(self.name) + + def delete(self, **kwargs): + """ + Delete an ePortfolio. + + :calls: `DELETE /api/v1/eportfolios/:id \ + `_ + + :returns: ePortfolio with deleted date set. + :rtype: :class:`canvasapi.eportfolio.EPortfolio` + """ + response = self._requester.request( + "DELETE", "eportfolios/{}".format(self.id), _kwargs=combine_kwargs(**kwargs) + ) + return EPortfolio(self._requester, response.json()) + + def get_eportfolio_pages(self, **kwargs): + """ + Return a list of pages for an ePortfolio. + + :calls: `GET /api/v1/eportfolios/:eportfolio_id/pages \ + `_ + + :returns: List of ePortfolio pages. + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.eportfolio.EPortfolioPage` + """ + + return PaginatedList( + EPortfolioPage, + self._requester, + "GET", + "eportfolios/{}/pages".format(self.id), + _kwargs=combine_kwargs(**kwargs), + ) + + def moderate_eportfolio(self, **kwargs): + """ + Update the spam_status of an eportfolio. + Only available to admins who can `moderate_user_content`. + + :calls: `PUT /api/v1/eportfolios/:eportfolio_id/moderate \ + `_ + + :returns: Updated ePortfolio. + :rtype: :class:`canvasapi.eportfolio.EPortfolio` + """ + response = self._requester.request( + "PUT", + "eportfolios/{}/moderate".format(self.id), + _kwargs=combine_kwargs(**kwargs), + ) + + return EPortfolio(self._requester, response.json()) + + def restore(self, **kwargs): + """ + Restore an ePortfolio back to active that was previously deleted. + Only available to admins who can moderate_user_content. + + :calls: `PUT /api/v1/eportfolios/:eportfolio_id/restore \ + `_ + + :returns: Updated ePortfolio. + :rtype: :class:`canvasapi.eportfolio.EPortfolio` + """ + response = self._requester.request( + "PUT", + "eportfolios/{}/restore".format(self.id), + _kwargs=combine_kwargs(**kwargs), + ) + + return EPortfolio(self._requester, response.json()) + + +class EPortfolioPage(CanvasObject): + def __str__(self): + return "{}. {}".format(self.position, self.name) diff --git a/canvasapi/user.py b/canvasapi/user.py index 6804e65e..4b42b713 100644 --- a/canvasapi/user.py +++ b/canvasapi/user.py @@ -25,7 +25,7 @@ def add_observee(self, observee_id, **kwargs): :param observee_id: The login id for the user to observe. :type observee_id: int - :rtype: :class: `canvasapi.user.User` + :rtype: :class:`canvasapi.user.User` """ response = self._requester.request( @@ -490,6 +490,25 @@ def get_enrollments(self, **kwargs): _kwargs=combine_kwargs(**kwargs), ) + def get_eportfolios(self, **kwargs): + """ + Returns a list of ePortfolios for a user. + + :calls: `GET /api/v1/users/:user_id/eportfolios \ + `_ + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.eportfolio.EPortfolio` + """ + from canvasapi.eportfolio import EPortfolio + + return PaginatedList( + EPortfolio, + self._requester, + "GET", + "users/{}/eportfolios".format(self.id), + _kwargs=combine_kwargs(**kwargs), + ) + def get_feature_flag(self, feature, **kwargs): """ Returns the feature flag that applies to the given user. @@ -866,6 +885,31 @@ def merge_into(self, destination_user, **kwargs): super(User, self).set_attributes(response.json()) return self + def moderate_all_eportfolios(self, **kwargs): + """ + Update the spam_status for all active eportfolios of a user. + Only available to admins who can moderate_user_content. + + :param eportfolio: The object or ID of the ePortfolio to retrieve. + :type eportfolio: :class:`canvasapi.eportfolio.EPortfolio` or int + + :calls: `PUT /api/v1/users/:user_id/eportfolios \ + `_ + + :returns: A list of all user ePortfolios. + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.eportfolio.EPortfolio` + """ + from canvasapi.eportfolio import EPortfolio + + return PaginatedList( + EPortfolio, + self._requester, + "PUT", + "users/{}/eportfolios".format(self.id), + _kwargs=combine_kwargs(**kwargs), + ) + def remove_observee(self, observee_id, **kwargs): """ Unregisters a user as being observed by the given user. @@ -875,7 +919,7 @@ def remove_observee(self, observee_id, **kwargs): :param observee_id: The login id for the user to observe. :type observee_id: int - :rtype: :class: `canvasapi.user.User` + :rtype: :class:`canvasapi.user.User` """ response = self._requester.request( @@ -963,7 +1007,7 @@ def show_observee(self, observee_id, **kwargs): :param observee_id: The login id for the user to observe. :type observee_id: int - :rtype: :class: `canvasapi.user.User` + :rtype: :class:`canvasapi.user.User` """ response = self._requester.request( diff --git a/docs/class-reference.rst b/docs/class-reference.rst index ef92cd24..21c0f2c7 100644 --- a/docs/class-reference.rst +++ b/docs/class-reference.rst @@ -27,6 +27,7 @@ Class Reference discussion-topic-ref enrollment-ref enrollment-term-ref + eportfolio-ref external-tool-ref favorite-ref feature-ref diff --git a/docs/eportfolio-ref.rst b/docs/eportfolio-ref.rst new file mode 100644 index 00000000..922d5769 --- /dev/null +++ b/docs/eportfolio-ref.rst @@ -0,0 +1,13 @@ +========== +EPortfolio +========== + +.. autoclass:: canvasapi.eportfolio.EPortfolio + :members: + +============== +EPortfolioPage +============== + +.. autoclass:: canvasapi.eportfolio.EPortfolioPage + :members: diff --git a/tests/fixtures/eportfolio.json b/tests/fixtures/eportfolio.json new file mode 100644 index 00000000..95f54cd5 --- /dev/null +++ b/tests/fixtures/eportfolio.json @@ -0,0 +1,75 @@ +{ + "delete_eportfolio": { + "method": "DELETE", + "endpoint": "eportfolios/1", + "data": { + "id": 1, + "name": "ePortfolio 1", + "workflow_state": "deleted", + "description": "Delete this ePortfolio", + "deleted_at": "2022-07-05T21:00:00Z" + }, + "status_code": 200 + }, + "get_eportfolio_by_id": { + "method": "GET", + "endpoint": "eportfolios/1", + "data": { + "id": 1, + "user_id": 1, + "workflow_state": "active", + "name": "ePortfolio 1", + "deleted_at": "null", + "spam_status": "null" + }, + "status_code": 200 + }, + "get_eportfolio_pages": { + "method": "GET", + "endpoint": "eportfolios/1/pages", + "data": [ + { + "id": 1, + "eportfolio_id": 1, + "position": 1, + "name": "ePortfolio 1", + "content": "This is the page of content", + "created_at": "2022-07-01T18:00:00Z", + "updated_at": "2021-07-04T18:00:00Z" + }, + { + "id": 2, + "eportfolio_id": 1, + "position": 2, + "name": "ePortfolio 1", + "content": "This is the second page of content", + "created_at": "2022-07-02T18:00:00Z", + "updated_at": "2021-07-02T18:00:00Z" + } + ], + "status_code": 200 + }, + "moderate_eportfolio_as_spam": { + "method": "PUT", + "endpoint": "eportfolios/1/moderate", + "data": { + "id": 1, + "user_id": 1, + "workflow_state": "active", + "name": "ePortfolio 1", + "deleted_at": "null", + "spam_status": "marked_as_spam" + } + }, + "restore_deleted_eportfolio": { + "method": "PUT", + "endpoint": "eportfolios/1/restore", + "data": { + "id": 1, + "user_id": 1, + "workflow_state": "active", + "name": "ePortfolio 1", + "deleted_at": "null" + } + } +} diff --git a/tests/fixtures/user.json b/tests/fixtures/user.json index babaff54..557bf330 100644 --- a/tests/fixtures/user.json +++ b/tests/fixtures/user.json @@ -544,6 +544,24 @@ ], "status_code": 200 }, + "moderate_user_eportfolios": { + "method": "PUT", + "endpoint": "users/1/eportfolios", + "data": [ + { + "name": "ePortfolio 1", + "workflow_state": "active", + "created_at": "2022-07-05T21:00:00Z", + "spam_status": "marked_as_spam" + }, + { + "name": "ePortfolio 2", + "workflow_state": "active", + "created_at": "2022-07-05T21:00:00Z", + "spam_status": "marked_as_spam" + } + ] + }, "page_views": { "method": "GET", "endpoint": "users/1/page_views", @@ -1055,6 +1073,44 @@ }, "status_code": 200 }, + "get_eportfolios": { + "method": "GET", + "endpoint": "users/1/eportfolios", + "data": [ + { + "name": "ePortfolio 1", + "workflow_state": "active", + "created_at": "2022-07-05T21:00:00Z" + }, + { + "name": "ePortfolio 2", + "workflow_state": "active", + "created_at": "2022-07-05T21:00:00Z" + } + ] + }, + "get_eportfolios_include_deleted": { + "method": "GET", + "endpoint": "users/1/eportfolios", + "data": [ + { + "name": "ePortfolio 1", + "workflow_state": "active", + "created_at": "2022-07-05T21:00:00Z" + }, + { + "name": "ePortfolio 2", + "workflow_state": "active", + "created_at": "2022-07-05T21:00:00Z" + }, + { + "name": "ePortfolio 3", + "workflow_state": "deleted", + "created_at": "2022-07-05T21:00:00Z", + "deleted_at": "2022-07-06T18:00:00Z" + } + ] + }, "get_features": { "method": "GET", "endpoint": "users/1/features", diff --git a/tests/test_canvas.py b/tests/test_canvas.py index 84be6e24..35ece62b 100644 --- a/tests/test_canvas.py +++ b/tests/test_canvas.py @@ -14,6 +14,7 @@ from canvasapi.course import Course, CourseNickname from canvasapi.course_epub_export import CourseEpubExport from canvasapi.discussion_topic import DiscussionTopic +from canvasapi.eportfolio import EPortfolio from canvasapi.exceptions import RequiredFieldMissing, ResourceDoesNotExist from canvasapi.file import File from canvasapi.group import Group, GroupCategory @@ -779,6 +780,15 @@ def test_course_announcements_legacy(self, m): self.assertEqual(announcements[0]._parent_type, "course") self.assertEqual(announcements[0]._parent_id, "1") + # get_eportfolio() + def test_get_eportfolio(self, m): + register_uris({"eportfolio": ["get_eportfolio_by_id"]}, m) + + eportfolio = self.canvas.get_eportfolio(1) + + self.assertIsInstance(eportfolio, EPortfolio) + self.assertEqual(eportfolio.name, "ePortfolio 1") + # get_epub_exports() def test_get_epub_exports(self, m): diff --git a/tests/test_eportfolio.py b/tests/test_eportfolio.py new file mode 100644 index 00000000..92b71aba --- /dev/null +++ b/tests/test_eportfolio.py @@ -0,0 +1,73 @@ +import unittest + +import requests_mock + +from canvasapi import Canvas +from canvasapi.eportfolio import EPortfolio, EPortfolioPage +from canvasapi.paginated_list import PaginatedList +from tests import settings +from tests.util import register_uris + + +@requests_mock.Mocker() +class TestEPortfolio(unittest.TestCase): + def setUp(self): + self.canvas = Canvas(settings.BASE_URL, settings.API_KEY) + + with requests_mock.Mocker() as m: + register_uris({"eportfolio": ["get_eportfolio_by_id"]}, m) + + self.eportfolio = self.canvas.get_eportfolio(1) + + def test_str(self, m): + eportfolio_string = str(self.eportfolio) + self.assertEqual(eportfolio_string, "ePortfolio 1") + + def test_delete_eportfolio(self, m): + register_uris({"eportfolio": ["delete_eportfolio"]}, m) + + deleted_eportfolio = self.eportfolio.delete() + + self.assertIsInstance(deleted_eportfolio, EPortfolio) + self.assertEqual(deleted_eportfolio.deleted_at, "2022-07-05T21:00:00Z") + self.assertEqual(deleted_eportfolio.workflow_state, "deleted") + + def test_get_eportfolio_pages(self, m): + register_uris({"eportfolio": ["get_eportfolio_pages"]}, m) + + pages = self.eportfolio.get_eportfolio_pages() + + string_page = str(pages[0]) + + self.assertIsInstance(pages, PaginatedList) + self.assertIsInstance(pages[0], EPortfolioPage) + self.assertIsInstance(pages[1], EPortfolioPage) + self.assertEqual(pages[0].position, 1) + self.assertEqual(pages[1].position, 2) + self.assertEqual(string_page, "1. ePortfolio 1") + + def test_moderate_eportfolio_as_spam(self, m): + register_uris({"eportfolio": ["moderate_eportfolio_as_spam"]}, m) + + spam_eportfolio = self.eportfolio.moderate_eportfolio( + spam_status="marked_as_spam" + ) + + self.assertIsInstance(spam_eportfolio, EPortfolio) + self.assertEqual(spam_eportfolio.spam_status, "marked_as_spam") + + def test_restore_deleted_eportfolio(self, m): + register_uris( + {"eportfolio": ["delete_eportfolio", "restore_deleted_eportfolio"]}, m + ) + + eportfolio = self.eportfolio.delete() + + self.assertIsInstance(eportfolio, EPortfolio) + self.assertEqual(eportfolio.workflow_state, "deleted") + self.assertEqual(eportfolio.deleted_at, "2022-07-05T21:00:00Z") + + restored = eportfolio.restore() + + self.assertEqual(restored.workflow_state, "active") + self.assertEqual(restored.deleted_at, "null") diff --git a/tests/test_user.py b/tests/test_user.py index f6574f5c..9093b783 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -13,6 +13,7 @@ from canvasapi.content_migration import ContentMigration, Migrator from canvasapi.course import Course from canvasapi.enrollment import Enrollment +from canvasapi.eportfolio import EPortfolio from canvasapi.feature import Feature, FeatureFlag from canvasapi.file import File from canvasapi.folder import Folder @@ -279,6 +280,40 @@ def test_create_communication_channels(self, m): self.assertIsInstance(new_channel, CommunicationChannel) + # get_eportfolios() + def test_get_eportfolios(self, m): + register_uris({"user": ["get_eportfolios"]}, m) + + eportfolios = self.user.get_eportfolios() + eportfolio_list = [portfolio for portfolio in eportfolios] + self.assertIsInstance(eportfolios, PaginatedList) + self.assertIsInstance(eportfolios[0], EPortfolio) + self.assertEqual(len(eportfolio_list), 2) + self.assertEqual(eportfolios[0].name, "ePortfolio 1") + + def test_get_eportfolios_with_deleted(self, m): + register_uris({"user": ["get_eportfolios_include_deleted"]}, m) + + eportfolios = self.user.get_eportfolios() + eportfolio_list = [portfolio for portfolio in eportfolios] + self.assertIsInstance(eportfolios, PaginatedList) + self.assertIsInstance(eportfolios[0], EPortfolio) + self.assertEqual(len(eportfolio_list), 3) + self.assertEqual(eportfolios[2].name, "ePortfolio 3") + self.assertEqual(eportfolios[2].workflow_state, "deleted") + + def test_moderate_user_eportfolios(self, m): + register_uris({"user": ["moderate_user_eportfolios"]}, m) + + spam_eportfolios = self.user.moderate_all_eportfolios( + spam_status="marked_as_spam" + ) + eportfolio_list = [portfolio for portfolio in spam_eportfolios] + self.assertIsInstance(spam_eportfolios, PaginatedList) + self.assertIsInstance(spam_eportfolios[0], EPortfolio) + self.assertEqual(len(eportfolio_list), 2) + self.assertEqual(spam_eportfolios[0].spam_status, "marked_as_spam") + # get_files() def test_get_files(self, m): register_uris({"user": ["get_user_files", "get_user_files2"]}, m)