diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..83a80fd5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +language: python +python: + - 2.7 +install: + - pip install -r tests_requirements.txt + - pip install coveralls +script: + - coverage run -m unittest discover + - pycodestyle canvasapi tests + - pyflakes canvasapi tests +after_success: + - coveralls +notifications: + slack: + secure: oLrAMXKVN9EKOyvROg5ZZUQgeleKq6lsVslm/Jtg516LtdBnLiAA672xMrwB0m8UtSJvoz4m1BrBQBeO+OzxlmioJHDvm//MMXv8wpfBdi18ojBA/5wjYfYP4eta/9j1x4myWH9ygcJOdKhALOSt4fwKsmIRDt6YTensOaqcCTiS2YjtAWMLrKvlathpdMokx0k7FVluJJ+zEfzNote75AP7yq6nFFe9j0ALX7YuNFytshZXi82aKZpAuUGMNLjcL+B5acLKSYqF214sTVETPa2mrhja+89+W9512X3ecRdHPC6Ud10ICD6ap0XMrOcgNW8XREV0pDIDPrMJgSUFiFWMhvrkMjfnmJB2nrINPTc3CrMmmk8Hk1XYta+iA4yAEUb9dhkKmkxMZWtn7TZ7DiKaAXznwj6poGrnN+NXeWfoJ0j2pAUrcO4Jn9e43+ORMDYqCFV3QemtykbX0HKgVfOTx5F1ZBGULiMi7U3q5naieGep5j+OxNI9bcQMUoM6O+VxmkjtB1/8/F7PlKHrA/7Z1+s+1HM00lSvw5qAhkInPgkLU+X495+TsEfqJuVZIUIAS1jDDSuo3bTHHUYzDwE9UF/dYN/nVDiUEVa4xYe+UrAFuPnezL/bdezVwzoGd1Mb5HKJxs90wxYIZFkReivA1z4bQsYh+A/QbYKewR0= diff --git a/AUTHORS.md b/AUTHORS.md index 5f70b409..61cf0821 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -12,5 +12,8 @@ Patches and Suggestions - Devin Singh [@devints47](https://github.com/devints47) - Elise Heron [@thedarkestknight](https://github.com/thedarkestknight) - Ian Turgeon [@iturgeon](https://github.com/iturgeon) +- John Raible [@rebelaide](https://github.com/rebelaide) +- Nathan Dabu [@nathaned](https://github.com/nathaned) - Philip Carter [@phillyc](https://github.com/phillyc) +- Tuan Pham [@tuanvpham](https://github.com/tuanvpham) - William Funk [@WilliamRADFunk](https://github.com/WilliamRADFunk) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67d2424d..96265f78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Change Log +## [0.4.0] - 2017-06-16 +### New Endpoint Coverage + +- Analytics +- Announcement External Feeds +- Authentication Providers +- Communications Channels +- Files +- Logins +- Notification Preferences +- Submissions +- Search +- Tabs +- User Observees + +### General + +- Set up TravisCI and Coveralls. +- Added Badges to README. +- Updated CONTRIBUTING.md to more accurately reflect our dev process. + ## [0.3.0] - 2017-03-30 ### New Endpoint Coverage @@ -65,6 +86,7 @@ - Fixed some incorrectly defined parameters - Fixed an issue where tests would fail due to an improperly configured requires block +[0.4.0]: https://github.com/ucfopen/canvasapi/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/ucfopen/canvasapi/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/ucfopen/canvasapi/compare/v0.1.2...v0.2.0 [0.1.2]: https://github.com/ucfopen/canvasapi/compare/v0.1.1...v0.1.2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6568684f..f1f90d90 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Contributing to canvasapi +# Contributing to CanvasAPI Thanks for your interest in contributing! @@ -7,39 +7,44 @@ Below you'll find guidelines for contributing that will keep our codebase clean ## Table of Contents * [How can I contribute?](#how-can-i-contribute) - * [Bug reports](#bug-reports) - * [Resolving issues](#resolving-issues) -* [Making your first contribution](#making-your-first-contribution) - * [Setting up the environment](#setting-up-the-environment) - * [Writing tests](#writing-tests) - * [Running tests / coverage reports](#running-tests-coverage-reports) + * [Bug reports](#bug-reports) + * [Resolving issues](#resolving-issues) + * [Making your first contribution](#making-your-first-contribution) + * [Setting up the environment](#setting-up-the-environment) + * [Writing tests](#writing-tests) + * [API Coverage Tests](#api-coverage-tests) + * [Engine tests](#engine-tests) + * [Running tests / coverage reports](#running-tests-coverage-reports) * [Code style guidelines](#code-style-guidelines) - * [Foolish consistency](#foolish-consistency) - * [Method docstrings](#method-docstrings) - * [Docstring examples](#docstring-examples) + * [Foolish consistency](#foolish-consistency) + * [Method docstrings](#method-docstrings) + * [Descriptions](#descriptions) + * [Links to related API endpoints](#links-to-related-api-endpoints) + * [Parameters](#parameters) + * [Returns](#returns) + * [Docstring examples](#docstring-examples) ## How can I contribute? ### Bug Reports -#### Reporting bugs Bug reports are awesome. Writing quality bug reports helps us identify issues and solve them even faster. You can submit bug reports directly to our [issue tracker](https://github.com/ucfopen/canvasapi/issues). Here are a few things worth mentioning when making a report: -* What **version** of canvasapi are you running? (Use `pip list` -- we try to build frequently so "latest" isn't always accurate.) +* What **version** of CanvasAPI are you running? (Use `pip show canvasapi` -- we try to build frequently so "latest" isn't always accurate.) * What steps can be taken to **reproduce the issue**? * **Detail matters.** Try not to be too be verbose, but generally the more information, the better! ### Resolving issues -We welcome pull requests for bug fixes and new features! Feel free to browse our open, unassigned issues and assign yourself to them. You can also filter by labels: -* [simple](https://github.com/ucfopen/canvasapi/issues?scope=all&sort=id_desc&state=opened&utf8=%E2%9C%93&label_name%5B%5D=simple) -- easier issues to start working on; great for getting familiar with the codebase. -* [api coverage](https://github.com/ucfopen/canvasapi/issues?scope=all&sort=id_desc&state=opened&utf8=%E2%9C%93&label_name%5B%5D=api+coverage) -- covering new endpoints or updating existing ones. -* [internal](https://github.com/ucfopen/canvasapi/issues?scope=all&sort=id_desc&state=opened&utf8=%E2%9C%93&label_name%5B%5D=internal) -- updates to the engine to improve performance. -* [major](https://github.com/ucfopen/canvasapi/issues?scope=all&sort=id_desc&state=opened&utf8=%E2%9C%93&label_name%5B%5D=major) -- difficult or major changes or additions that require familiarity with the library. -* [bug](https://github.com/ucfopen/canvasapi/issues?scope=all&sort=id_desc&state=opened&utf8=%E2%9C%93&label_name%5B%5D=bug) -- happy little code accidents. +We welcome pull requests for bug fixes and new features! Feel free to browse our open, unassigned issues and assign yourself to them. You can also filter by labels: +* [simple](https://github.com/ucfopen/canvasapi/issues?q=sort%3Aid_desc-desc+is%3Aopen+label%3Asimple) -- easier issues to start working on; great for getting familiar with the codebase. +* [api coverage](https://github.com/ucfopen/canvasapi/issues?q=sort%3Aid_desc-desc+is%3Aopen+label%3Aapi-coverage) -- covering new endpoints or updating existing ones. +* [internal](https://github.com/ucfopen/canvasapi/issues?q=sort%3Aid_desc-desc+is%3Aopen+label%3Ainternal) -- updates to the engine to improve performance. +* [major](https://github.com/ucfopen/canvasapi/issues?q=sort%3Aid_desc-desc+is%3Aopen+label%3Amajor) -- difficult or major changes or additions that require familiarity with the library. +* [bug](https://github.com/ucfopen/canvasapi/issues?q=sort%3Aid_desc-desc+is%3Aopen+label%3Abug) -- happy little code accidents. Once you've found an issue you're interested in tackling, take a look at our [first contribution tutorial](#making-your-first-contribution) for information on our pull request policy. @@ -63,88 +68,100 @@ Tests are a critical part of building applications, and we [pity the fool who do You'll notice our tests live in the creatively named `tests` directory. Within that directory, you'll see several files in the form `test_[class].py` and another directory named `fixtures`. Depending on the scope of the issue you're solving, you'll be writing two different kinds of tests. -##### API coverage tests +##### API Coverage Tests -We use the [requests-mock](https://pypi.python.org/pypi/requests-mock) library to simulate API responses, and those mock responses live inside the `fixtures` directory in JSON files. Each file's name describes the endpoints that are contained within (course endpoints live in `course.json`, for example). Those fixtures are loaded by name within a test Python file. Let's look at `test_course.py`: +We use the [requests-mock](https://pypi.python.org/pypi/requests-mock) library to simulate API responses. Those mock responses live inside the `fixtures` directory in JSON files. Each file's name describes the endpoints that are contained within. For example, course endpoints live in `course.json`. These fixtures are loaded on demand in a given test. Let's look at `test_get_user` in `test_course.py` as an example: ```python - @classmethod - def setUpClass(self): - requires = { - 'course': [ - 'create', 'create_assignment', 'deactivate_enrollment', - 'enroll_user', 'get_all_assignments', 'get_all_assignments2', - 'get_assignment_by_id' - ], - 'external_tool': ['get_by_id_course'], - 'quiz': ['get_by_id'], - 'user': ['get_by_id'] - } - - adapter = requests_mock.Adapter() - self.canvas = Canvas(settings.BASE_URL, settings.API_KEY, adapter) - register_uris(settings.BASE_URL, requires, adapter) -``` +# get_user() +def test_get_user(self, m): + register_uris({'course': ['get_user']}, m) -This file tests several different endpoints, all of which are included in a `requires` dict and then loaded by the `register_uris()` function in `tests.util`. The keys in the dictionary are the file names of the JSON files (without the extension) and the values are lists with elements that match the related keys in the JSON file. -Once they're loaded with `register_uris()`, any attempt to access a URL that matches will return the local fixture rather than trying to access a remote server. + user = self.course.get_user(1) -Let's look at the first required fixture, `course create_quiz`: + self.assertIsInstance(user, User) + self.assertTrue(hasattr(user, 'name')) +``` +Breakdown: + +```python +# get_user() ``` -"create": { - "method": "POST", - "endpoint": "courses/1/quizzes", - "data": { - "id": 1, - "title": "Newer Title" - }, - "status_code": 200 -} + +It is common to have multiple tests for a single method. All related tests should be grouped together under a single comment with the name of the method being tested. + +--- + +```python +def test_get_user(self, m): ``` -While we're at it, let's pull out the code in the library for creating a quiz: +This is a standard Python `unittest` test method with one addition: the `m` variable is passed to all methods with names starting with `test`. `m` is a Mocker object that can be used to override the routing of HTTP requests. + +--- ```python -def create_quiz(self, title, **kwargs): - from quiz import Quiz - response = self._requester.request( - 'POST', - 'courses/%s/quizzes' % (self.id), - **combine_kwargs(**kwargs) - ) +register_uris({'course': ['get_user']}, m) ``` -This code sends a POST request to the URL `courses/:course_id/quizzes`. With that information, we know that our fixture needs to contain `"method": "POST"` and `"endpoint": "courses/1/quizzes"`. You may be wondering where the ID (`courses/1`) came from. In our setUpClass method in `test_course.py`, we define a few starting objects to work with: +The `register_uris` function tells a mocker object which fixtures to load. It takes in two arguments: a dictionary describing which fixtures to load, and a mocker object. The dictionary keys represent which file the desired fixtures are located in. The values are lists containing each desired fixture from that particular file. The example above will register the `get_user` fixture in `course.json`. + +Example Fixture: + +```json +"get_user": { + "method": "GET", + "endpoint": "courses/1/users/1", + "data": { + "id": 1, + "name": "John Doe" + }, + "status_code": 200 +}, +``` + +When this fixture is loaded, all `GET` requests to a url matching `courses/1/users/1` will return a status code of 200 and the provided user data for John Doe. + +--- ```python -self.course = self.canvas.get_course(1) -self.quiz = self.course.get_quiz(1) -self.user = self.canvas.get_user(1) +user = self.course.get_user(1) + +self.assertIsInstance(user, User) +self.assertTrue(hasattr(user, 'name')) ``` +The rest is basic unit testing. Call the function to be tested, and assert various outcomes. If necessary, multiple tests can written for a single method. All related tests should appear together under the same comment, as described earlier. -For consistency, it's easiest to call give your fixture objects an ID of 1 unless you need a second object. +--- -In the actual test, we use the `create_quiz()` method of `Course` to create a quiz with some data: +It is common to need certain object(s) for multiple tests. For example, most methods in `test_course.py` require a `Course` object. In this case, save a course to the class in `self.course` for later use. +Do this in the `setUp` class method: ```python -# create_quiz() -def test_create_quiz(self): - title = 'Newer Title' - new_quiz = self.course.create_quiz(self.course.id, quiz={'title': title}) - - self.assertIsInstance(new_quiz, Quiz) - self.assertTrue(hasattr(new_quiz, 'title')) - self.assertEqual(new_quiz.title, title) - self.assertTrue(hasattr(new_quiz, 'course_id')) - self.assertEqual(new_quiz.course_id, self.course.id) +with requests_mock.Mocker() as m: + requires = { + 'course': ['get_by_id', 'get_page'], + 'quiz': ['get_by_id'], + 'user': ['get_by_id'] + } + register_uris(requires, m) + + self.course = self.canvas.get_course(1) + self.page = self.course.get_page('my-url') + self.quiz = self.course.get_quiz(1) + self.user = self.canvas.get_user(1) ``` -Take a look at the existing tests to get a feel for the process. Once you've written a few, it should be second nature. +Since `setUp` is not a test method, it does not automatically get passed a Mocker object `m`. To use the mocker, all relevant code needs to be inside a `with` statement: + +```python +with requests_mock.Mocker() as m: +``` ##### Engine tests -Not all of canvasapi relies on networking. While these pieces are few and far between, we still need to verify that they're performing correctly. Writing tests for engine-level code is just as important as user-facing code and is a bit easier. You'll just need to follow the same process as you would for API tests, minus the fixtures. +Not all of CanvasAPI relies on networking. While these pieces are few and far between, we still need to verify that they're performing correctly. Writing tests for engine-level code is just as important as user-facing code and is a bit easier. You'll just need to follow the same process as you would for API tests, minus the fixtures. #### Running tests / coverage reports @@ -155,33 +172,23 @@ You'll do this by running `coverage run -m unittest discover` from the main `can Coverage reports tell us how much of our code is actually being tested. As of right now, we're happily maintaining 100% code coverage (🎉!) and our goal is to keep it there. Ensure you've covered your changes entirely by running `coverage report`. Your output should look something like this: ``` -Name Stmts Miss Cover ------------------------------------------------- -canvasapi/__init__.py 14 0 100% -canvasapi/account.py 83 0 100% -canvasapi/assignment.py 11 0 100% -canvasapi/avatar.py 2 0 100% -canvasapi/canvas.py 60 0 100% -canvasapi/canvas_object.py 20 0 100% -canvasapi/course.py 126 0 100% -canvasapi/enrollment.py 2 0 100% -canvasapi/exceptions.py 10 0 100% -canvasapi/external_tool.py 33 0 100% -canvasapi/module.py 62 0 100% -canvasapi/page_view.py 4 0 100% -canvasapi/paginated_list.py 66 0 100% -canvasapi/quiz.py 15 0 100% -canvasapi/requester.py 42 0 100% -canvasapi/section.py 9 0 100% -canvasapi/user.py 48 0 100% -canvasapi/util.py 22 0 100% ------------------------------------------------- -TOTAL 629 0 100% +Name Stmts Miss Cover +---------------------------------------------------- +canvasapi/__init__.py 3 0 100% +canvasapi/account.py 166 0 100% +canvasapi/appointment_group.py 17 0 100% +canvasapi/assignment.py 24 0 100% +[...] +canvasapi/upload.py 29 0 100% +canvasapi/user.py 101 0 100% +canvasapi/util.py 29 0 100% +---------------------------------------------------- +TOTAL 1586 0 100% ``` -Certain statements can be omitted from the coverage report by adding `# pragma: no cover` but this should be used conservatively. If your tests pass and your coverage is at 100%, you're ready to [submit a pull request](https://github.com/ucfopen/canvasapi/merge_requests)! +Certain statements can be omitted from the coverage report by adding `# pragma: no cover` but this should be used conservatively. If your tests pass and your coverage is at 100%, you're ready to [submit a pull request](https://github.com/ucfopen/canvasapi/pulls)! -Be sure to include the issue number in the title with a pound sign in front of it (#123) so we know which issue the code is addressing. Point the branch at master and then submit it for review. +Be sure to include the issue number in the title with a pound sign in front of it (#123) so we know which issue the code is addressing. Point the branch at `develop` and then submit it for review. ## Code Style Guidelines @@ -189,11 +196,17 @@ Be sure to include the issue number in the title with a pound sign in front of i We try to adhere to Python's [PEP 8](https://www.python.org/dev/peps/pep-0008/) specification as much as possible. In short, that means: * We use four spaces for indentation. -* All Python files end with an empty new line. -* Two spaces before a class declaration, one space before a function declaration. -* Lines should be around 80 characters long. Once you get into the 85+ territory, consider breaking your code into separate lines. +* Lines should be around 80 characters long, but up to 99 is allowed. Once you get into the 85+ territory, consider breaking your code into separate lines. -It's a good idea to set up a Python linter for your text editor to point out errors. +We use `pycodestyle` and `pyflakes` for linting: + +``` +pycodestyle canvasapi tests +``` + +``` +pyflakes canvasapi tests +``` ### Foolish consistency @@ -210,11 +223,11 @@ Method docstrings should include a description, a link to the related API endpoi A description should be a concise, *action* statement (use "*write* a good docstring" over "*writes* a good docstring") that describes the method. Generally, the official API documentation's description is usable (make sure it's an **action statement** though). Special functionality should be documented. #### Links to related API endpoints -A link to a related API endpoint is denoted with `:calls:`. canvasapi uses Sphinx to automatically generate documentation, so we can provide a link to an API endpoint with the reStructuredText syntax: +A link to a related API endpoint is denoted with `:calls:`. CanvasAPI uses Sphinx to automatically generate documentation, so we can provide a link to an API endpoint with the reStructuredText syntax: ``` :calls: `THE TEXT OF THE HYPERLINK \ - `_ + `_ ``` Hyperlink text should match the text underneath the endpoint in the official Canvas API documentation. Generally, that looks like this: @@ -228,8 +241,8 @@ Hyperlink text should match the text underneath the endpoint in the official Can #### Parameters Parameters should be listed in the order that they appear in the method prototype. They should take on the following form: ``` - :param PARAMETER_NAME: PARAMETER_DESCRIPTION. - :type PARAMETER_NAME: PYTHON_TYPE +:param PARAMETER_NAME: PARAMETER_DESCRIPTION. +:type PARAMETER_NAME: PYTHON_TYPE ``` #### Returns @@ -237,17 +250,17 @@ Parameters should be listed in the order that they appear in the method prototyp ```python def uncheck_box(box_id): - """ - Uncheck the box with the given ID. - - :returns: True if the box was successfully unchecked, False otherwise. - :rtype: bool - """ + """ + Uncheck the box with the given ID. + + :returns: True if the box was successfully unchecked, False otherwise. + :rtype: bool + """ ``` -In most cases, the return value is easy to infer based on the type and the description given in the docstring. Only use `:returns:` to clarify ambiguous cases (usually relating to boolean returns). +In most cases, the return value is easy to infer based on the type and the description given in the docstring. `:returns:` is only necessary to clarify ambiguous cases. -**Return type** should always be included when a value is returned. If it's not a primitive type (int, str, bool, list, etc.) a fully-qualified class name should be included: +**Return type** should always be included when a value is returned. If it's not a primitive type (`int`, `str`, `bool`, `list`, etc.) a fully-qualified class name should be included: ``` :rtype: :class:`canvasapi.user.User` @@ -263,44 +276,44 @@ In the event a PaginatedList is returned: Here are some real world examples of how docstrings should be formatted: ```python - def get_account(self, account_id): - """ - Retrieve information on an individual account. +def get_account(self, account_id): + """ + Retrieve information on an individual account. - :calls: `GET /api/v1/accounts/:id \ - `_ + :calls: `GET /api/v1/accounts/:id \ + `_ - :param account_id: The ID of the account to retrieve. - :type account_id: int - :rtype: :class:`canvasapi.account.Account` - """ + :param account_id: The ID of the account to retrieve. + :type account_id: int + :rtype: :class:`canvasapi.account.Account` + """ ``` ```python - def get_accounts(self, **kwargs): - """ - List accounts that the current user can view or manage. +def get_accounts(self, **kwargs): + """ + List accounts that the current user can view or manage. - Typically, students and teachers will get an empty list in - response. Only account admins can view the accounts that they - are in. + Typically, students and teachers will get an empty list in + response. Only account admins can view the accounts that they + are in. - :calls: `GET /api/v1/accounts \ - `_ + :calls: `GET /api/v1/accounts \ + `_ - :rtype: :class:`canvasapi.paginated_list.PaginatedList` of :class:`canvasapi.account.Account` - """ + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of :class:`canvasapi.account.Account` + """ ``` ```python - def clear_course_nicknames(self): - """ - Remove all stored course nicknames. +def clear_course_nicknames(self): + """ + Remove all stored course nicknames. - :calls: `DELETE /api/v1/users/self/course_nicknames \ - `_ + :calls: `DELETE /api/v1/users/self/course_nicknames \ + `_ - :returns: True if the nicknames were cleared, False otherwise. - :rtype: bool - """ + :returns: True if the nicknames were cleared, False otherwise. + :rtype: bool + """ ``` diff --git a/README.md b/README.md index 5fc57cd6..344ff25a 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,120 @@ -# canvasapi -canvasapi is a Python package that allows for simple access to the Instructure Canvas API. +[![CanvasAPI on PyPI](https://img.shields.io/pypi/v/canvasapi.svg)](https://pypi.python.org/pypi/canvasapi) +[![License](https://img.shields.io/pypi/l/canvasapi.svg)](https://pypi.python.org/pypi/canvasapi) +[![Build Status](https://travis-ci.org/ucfopen/canvasapi.svg?branch=master)](https://travis-ci.org/ucfopen/canvasapi) +[![Coverage Status](https://coveralls.io/repos/github/ucfopen/canvasapi/badge.svg?branch=master)](https://coveralls.io/github/ucfopen/canvasapi?branch=master) +[![Join UCF Open Slack Discussions](https://ucf-open-slackin.herokuapp.com/badge.svg)](https://ucf-open-slackin.herokuapp.com/) + +# CanvasAPI +CanvasAPI is a Python library for accessing Instructure’s [Canvas LMS API](https://canvas.instructure.com/doc/api/index.html). The library enables developers to programmatically manage Canvas courses, users, gradebooks, and more. ## Installation +You can install CanvasAPI with pip: + `pip install canvasapi` -## Getting Started -The first thing to do is open a connection with Canvas. You will need to provide the URL for the API endpoint of your Canvas instance as well as a valid API key. +## Quickstart +Getting started with CanvasAPI is easy. + +Like most API clients, CanvasAPI exposes a single class that provides access to the rest of the API: `Canvas`. + +The first thing to do is instantiate a new `Canvas` object by providing your Canvas instance’s root API URL and a valid API key: ```python +# Import the Canvas class from canvasapi import Canvas -api_url = "https://example.com/api/v1/" # URL of API for your Canvas instance -api_key = "p@$$w0rd" # Your API key +# Canvas API URL +API_URL = "https://example.com/api/v1/" +# Canvas API key +API_KEY = "p@$$w0rd" -canvas = Canvas(api_url, api_key) +# Initialize a new Canvas object +canvas = Canvas(API_URL, API_KEY) ``` -You can now use `canvas` to begin making API calls. Here are some examples: -```python -course = canvas.get_course('1111111') -``` +You can now use `canvas` to begin making API calls. -```python -user = canvas.get_user('5555555') -print user.name -``` +### Working with Canvas Objects +CanvasAPI converts the JSON responses from the Canvas API into Python objects. These objects provide further access to the Canvas API. You can find a full breakdown of the methods these classes provide in our [class documentation](http://pythonhosted.org/canvasapi/class-reference.html). Below, you’ll find a few examples of common CanvasAPI use cases. -Some calls will return a `PaginatedList` object instead of a single object. +#### Course objects +Courses can be retrieved from the API: ```python -users = course.get_users() -print users -``` +# Grab course 123456 +>>> course = canvas.get_course(123456) -```python - +# Access the course's name +>>> course.name +'Test Course' + +# Update the course's name +>>> course.update(name='New Course Name') ``` -This `PaginatedList` object is iterable. However, it doesn't contain any data until called. Calls to the API are made as-needed and results are stored in the object. You can use it like this: +See our documentation on [keyword arguments](#keyword-arguments) for more information about how `course.update()` handles the `name` argument. +#### User objects +Individual users can be pulled from the API as well: ```python -for user in users: - print user.name +# Grab user 123 +>>> user = canvas.get_user(123) + +# Access the user's name +>>> user.name +'Test User' + +# Retrieve a list of courses the user is enrolled in +>>> courses = user.get_courses() + +# Grab a different user by their SIS ID +>>> sis_user = canvas.get_user('some_user', 'sis_login_id') ``` -You can also use indices: +#### Paginated Lists +Some calls, like the `user.get_courses()` call above, will request multiple objects from Canvas’s API. CanvasAPI collects these objects in a `PaginatedList` object. `PaginatedList` generally acts like a regular Python list. You can grab an element by index, iterate over it, and take a slice of it. + +**Warning**: `PaginatedList` lazily loads its elements. Unfortunately, there’s no way to determine the exact number of records Canvas will return without traversing the list fully. This means that `PaginatedList` isn’t aware of its own length and negative indexing is not currently supported. + +Let’s look at how we can use the `PaginatedList` returned by our `get_courses()` call: ```python -print users[2].name +# Retrieve a list of courses the user is enrolled in +>>> courses = user.get_courses() + +>>> print courses + + +# Access the first element in our list. +# +# You'll notice the first call takes a moment, but the next N-1 +# elements (where N = the per_page argument supplied; the default is 10) +# will be instantly accessible. +>>> print courses[0] +TST101 Test Course (1234567) + +# Iterate over our course list +>>> for course in courses: + print course + +TST101 Test Course 1 (1234567) +TST102 Test Course 2 (1234568) +TST103 Test Course 3 (1234569) + +# Take a slice of our course list +>>> courses[:2] +[TST101 Test Course 1 (1234567), TST102 Test Course 2 (1234568)] ``` -And even slices! +#### Keyword arguments + +Most of Canvas’s API endpoints accept a variety of arguments. CanvasAPI allows developers to insert keyword arguments when making calls to endpoints that accept arguments. ```python -for user in users[2:]: - print user.name -``` +# Get all of the active courses a user is currently enrolled in +>>> courses = user.get_courses(enrollment_status='active') + +# Get all of the courses that a user is enrolled in as a Teaching Assistant +>>> courses = user.get_courses(enrollment_type='TaEnrollment') -**Warning**: Presently, there is no way to determine the exact number of records Canvas might return without brute forcing through all the API calls. This means that `PaginatedList` is not aware of it's own length and negative indicies and slices (`users[-1]`, `users[-1:]`, etc.) are not possible at this time. +# Fetch 50 objects per page when making calls that return a PaginatedList +>>> courses = user.get_courses(per_page=50) +``` diff --git a/canvasapi/__init__.py b/canvasapi/__init__.py index b6a385f2..8ec3a648 100644 --- a/canvasapi/__init__.py +++ b/canvasapi/__init__.py @@ -4,4 +4,4 @@ __all__ = ["Canvas"] -__version__ = '0.3.0' +__version__ = '0.4.0' diff --git a/canvasapi/account.py b/canvasapi/account.py index af8b64c7..30fb7bdb 100644 --- a/canvasapi/account.py +++ b/canvasapi/account.py @@ -616,6 +616,305 @@ def list_enrollment_terms(self, **kwargs): **combine_kwargs(**kwargs) ) + def list_user_logins(self, **kwargs): + """ + Given a user ID, return that user's logins for the given account. + + :calls: `GET /api/v1/accounts/:account_id/logins \ + `_ + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.login.Login` + """ + from login import Login + + return PaginatedList( + Login, + self._requester, + 'GET', + 'accounts/%s/logins' % (self.id), + **combine_kwargs(**kwargs) + ) + + def create_user_login(self, user, login, **kwargs): + """ + Create a new login for an existing user in the given account + + :calls: `POST /api/v1/accounts/:account_id/logins \ + `_ + + :param user: The attributes of the user to create a login for + :type user: `dict` + :param login: The attributes of the login to create + :type login: `dict` + :rtype: :class:`canvasapi.login.Login` + """ + from login import Login + + if isinstance(user, dict) and 'id' in user: + kwargs['user'] = user + else: + raise RequiredFieldMissing(( + "user must be a dictionary with keys " + "'id'." + )) + + if isinstance(login, dict) and 'unique_id' in login: + kwargs['login'] = login + else: + raise RequiredFieldMissing(( + "login must be a dictionary with keys " + "'unique_id'." + )) + + response = self._requester.request( + 'POST', + 'accounts/%s/logins' % (self.id), + **combine_kwargs(**kwargs) + ) + return Login(self._requester, response.json()) + + def get_department_level_participation_data_with_given_term(self, term_id): + """ + Return page view hits all available or concluded courses in the given term + + :calls: `GET /api/v1/accounts/:account_id/analytics/terms/:term_id/activity \ + `_ + + :rtype: dict + """ + + response = self._requester.request( + 'GET', + 'accounts/%s/analytics/terms/%s/activity' % (self.id, term_id) + ) + return response.json() + + def get_department_level_participation_data_current(self): + """ + Return page view hits all available courses in the default term + + :calls: `GET /api/v1/accounts/:account_id/analytics/current/activity \ + `_ + + :rtype: dict + """ + + response = self._requester.request( + 'GET', + 'accounts/%s/analytics/current/activity' % (self.id) + ) + return response.json() + + def get_department_level_participation_data_completed(self): + """ + Return page view hits all concluded courses in the default term + + :calls: `GET /api/v1/accounts/:account_id/analytics/completed/activity \ + `_ + + :rtype: dict + """ + + response = self._requester.request( + 'GET', + 'accounts/%s/analytics/completed/activity' % (self.id) + ) + return response.json() + + def get_department_level_grade_data_with_given_term(self, term_id): + """ + Return the distribution of all available or concluded grades with the given term + + :calls: `GET /api/v1/accounts/:account_id/analytics/terms/:term_id/grades \ + `_ + + :rtype: dict + """ + + response = self._requester.request( + 'GET', + 'accounts/%s/analytics/terms/%s/grades' % (self.id, term_id) + ) + return response.json() + + def get_department_level_grade_data_current(self): + """ + Return the distribution of all available grades in the default term + + :calls: `GET /api/v1/accounts/:account_id/analytics/current/grades \ + `_ + + :rtype: dict + """ + + response = self._requester.request( + 'GET', + 'accounts/%s/analytics/current/grades' % (self.id) + ) + return response.json() + + def get_department_level_grade_data_completed(self): + """ + Return the distribution of all concluded grades in the default term + + :calls: `GET /api/v1/accounts/:account_id/analytics/completed/grades \ + `_ + + :rtype: dict + """ + + response = self._requester.request( + 'GET', + 'accounts/%s/analytics/completed/grades' % (self.id) + ) + return response.json() + + def get_department_level_statistics_with_given_term(self, term_id): + """ + Return numeric statistics about the department with the given term + + :calls: `GET /api/v1/accounts/:account_id/analytics/terms/:term_id/statistics \ + `_ + + :rtype: dict + """ + + response = self._requester.request( + 'GET', + 'accounts/%s/analytics/terms/%s/statistics' % (self.id, term_id) + ) + return response.json() + + def get_department_level_statistics_current(self): + """ + Return all available numeric statistics about the department in the default term + + :calls: `GET /api/v1/accounts/:account_id/analytics/current/statistics \ + `_ + + :rtype: dict + """ + + response = self._requester.request( + 'GET', + 'accounts/%s/analytics/current/statistics' % (self.id) + ) + return response.json() + + def get_department_level_statistics_completed(self): + """ + Return all available numeric statistics about the department in the default term + + :calls: `GET /api/v1/accounts/:account_id/analytics/current/statistics \ + `_ + + :rtype: dict + """ + + response = self._requester.request( + 'GET', + 'accounts/%s/analytics/completed/statistics' % (self.id) + ) + return response.json() + + def add_authentication_providers(self, **kwargs): + """ + Add external authentication providers for the account + + :calls: `POST /api/v1/accounts/:account_id/authentication_providers \ + `_ + + :rtype: :class:`canvasapi.authentication_provider.AuthenticationProvider` + """ + from canvasapi.authentication_provider import AuthenticationProvider + + response = self._requester.request( + 'POST', + 'accounts/%s/authentication_providers' % (self.id), + **combine_kwargs(**kwargs) + ) + authentication_providers_json = response.json() + authentication_providers_json.update({'account_id': self.id}) + + return AuthenticationProvider(self._requester, authentication_providers_json) + + def list_authentication_providers(self, **kwargs): + """ + Return the list of authentication providers + + :calls: `GET /api/v1/accounts/:account_id/authentication_providers \ + `_ + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.authentication_provider.AuthenticationProvider` + """ + from canvasapi.authentication_provider import AuthenticationProvider + + return PaginatedList( + AuthenticationProvider, + self._requester, + 'GET', + 'accounts/%s/authentication_providers' % (self.id), + {'account_id': self.id}, + **combine_kwargs(**kwargs) + ) + + def get_authentication_providers(self, authentication_providers_id, **kwargs): + """ + Get the specified authentication provider + + :calls: `GET /api/v1/accounts/:account_id/authentication_providers/:id \ + `_ + + :rtype: :class:`canvasapi.authentication_provider.AuthenticationProvider` + """ + from canvasapi.authentication_provider import AuthenticationProvider + + response = self._requester.request( + 'GET', + 'accounts/%s/authentication_providers/%s' % (self.id, authentication_providers_id), + **combine_kwargs(**kwargs) + ) + + return AuthenticationProvider(self._requester, response.json()) + + def show_account_auth_settings(self, **kwargs): + """ + Return the current state of each account level setting + + :calls: `GET /api/v1/accounts/:account_id/sso_settings \ + `_ + + :rtype: :class:`canvasapi.account.SSOSettings` + """ + + response = self._requester.request( + 'GET', + 'accounts/%s/sso_settings' % (self.id), + **combine_kwargs(**kwargs) + ) + + return SSOSettings(self._requester, response.json()) + + def update_account_auth_settings(self, **kwargs): + """ + Return the current state of account level after updated + + :calls: `PUT /api/v1/accounts/:account_id/sso_settings \ + `_ + + :rtype: :class:`canvasapi.account.SSOSettings` + """ + + response = self._requester.request( + 'PUT', + 'accounts/%s/sso_settings' % (self.id), + **combine_kwargs(**kwargs) + ) + + return SSOSettings(self._requester, response.json()) + class AccountNotification(CanvasObject): @@ -633,3 +932,9 @@ class Role(CanvasObject): def __str__(self): # pragma: no cover return "{} ({})".format(self.label, self.base_role_type) + + +class SSOSettings(CanvasObject): + + def __str___(self): # pragma: no cover + return"{} ({})".format(self.login_handle_name, self.change_password_url) diff --git a/canvasapi/authentication_provider.py b/canvasapi/authentication_provider.py new file mode 100644 index 00000000..3615d895 --- /dev/null +++ b/canvasapi/authentication_provider.py @@ -0,0 +1,43 @@ +from canvasapi.canvas_object import CanvasObject +from canvasapi.util import combine_kwargs + + +class AuthenticationProvider(CanvasObject): + + def __str__(self): # pragma: no cover + return "{} ({})".format(self.auth_type, self.position) + + def update(self, **kwargs): + """ + Update an authentication provider using the same options as the create endpoint + + :calls: `PUT /api/v1/accounts/:account_id/authentication_providers/:id \ + `_ + + :rtype: :class:`canvasapi.authentication_provider.AuthenticationProvider` + """ + response = self._requester.request( + 'PUT', + 'accounts/%s/authentication_providers/%s' % (self.account_id, self.id), + **combine_kwargs(**kwargs) + ) + + if response.json().get('auth_type'): + super(AuthenticationProvider, self).set_attributes(response.json()) + + return response.json().get('auth_type') + + def delete(self): + """ + Delete the config + + :calls: `DELETE /api/v1/accounts/:account_id/authentication_providers/:id \ + `_ + + :rtype: :class:`canvasapi.authentication_provider.AuthenticationProvider` + """ + response = self._requester.request( + 'DELETE', + 'accounts/%s/authentication_providers/%s' % (self.account_id, self.id) + ) + return AuthenticationProvider(self._requester, response.json()) diff --git a/canvasapi/canvas.py b/canvasapi/canvas.py index c19f2223..a4c06170 100644 --- a/canvasapi/canvas.py +++ b/canvasapi/canvas.py @@ -1,6 +1,7 @@ from canvasapi.account import Account from canvasapi.course import Course from canvasapi.exceptions import RequiredFieldMissing +from canvasapi.folder import Folder from canvasapi.group import Group, GroupCategory from canvasapi.paginated_list import PaginatedList from canvasapi.requester import Requester @@ -755,3 +756,58 @@ def list_group_participants(self, appointment_group_id, **kwargs): 'appointment_groups/%s/groups' % (appointment_group_id), **combine_kwargs(**kwargs) ) + + def get_folder(self, folder_id): + """ + Returns the details for a folder + + :calls: `GET /api/v1/folders/:id \ + `_ + + :param folder_id: The ID of the folder to retrieve. + :type folder_id: int + :rtype: :class:`canvasapi.folder.Folder` + """ + response = self.__requester.request( + 'GET', + 'folders/%s' % (folder_id) + ) + return Folder(self.__requester, response.json()) + + def search_recipients(self, **kwargs): + """ + Find valid recipients (users, courses and groups) that the current user + can send messages to. + Returns a list of mixed data types. + + :calls: `GET /api/v1/search/recipients \ + `_ + + :rtype: `list` + """ + if 'search' not in kwargs: + kwargs['search'] = ' ' + + response = self.__requester.request( + 'GET', + 'search/recipients', + **combine_kwargs(**kwargs) + ) + return response.json() + + def search_all_courses(self, **kwargs): + """ + List all the courses visible in the public index. + Returns a list of dicts, each containing a single course. + + :calls: `GET /api/v1/search/all_courses \ + `_ + + :rtype: `list` + """ + response = self.__requester.request( + 'GET', + 'search/all_courses', + **combine_kwargs(**kwargs) + ) + return response.json() diff --git a/canvasapi/communication_channel.py b/canvasapi/communication_channel.py new file mode 100644 index 00000000..cd8984a7 --- /dev/null +++ b/canvasapi/communication_channel.py @@ -0,0 +1,71 @@ +from canvasapi.canvas_object import CanvasObject +from canvasapi.notification_preference import NotificationPreference + + +class CommunicationChannel(CanvasObject): + + def __str__(self): + return "{} ({})".format(self.address, self.id) + + def list_preferences(self): + """ + Fetch all preferences for the given communication channel. + + :calls: `GET + /api/v1/users/:user_id/communication_channels/:cc_id/notification_preferences \ + `_ + + :rtype: `list` + """ + response = self._requester.request( + 'GET', + 'users/%s/communication_channels/%s/notification_preferences' % ( + self.user_id, + self.id + ) + ) + return response.json()['notification_preferences'] + + def list_preference_categories(self): + """ + Fetch all notification preference categories for the given communication + channel. + + :calls: `GET + /api/v1/users/:u_id/communication_channels/:cc_id/notification_preference_categories \ + `_ + + :rtype: `list` + """ + response = self._requester.request( + 'GET', + 'users/%s/communication_channels/%s/notification_preference_categories' % ( + self.user_id, + self.id + ) + ) + return response.json()['categories'] + + def get_preference(self, notification): + """ + Fetch the preference for the given notification for the given + communication channel. + + :calls: `GET + /api/v1/users/:u_id/communication_channels/:co_id/notification_preferences/:notif \ + `_ + + :param notification: The name of the notification. + :type notification: str + :rtype: :class:`canvasapi.notification_preference.NotificationPreference` + """ + response = self._requester.request( + 'GET', + 'users/%s/communication_channels/%s/notification_preferences/%s' % ( + self.user_id, + self.id, + notification + ) + ) + data = response.json()['notification_preferences'][0] + return NotificationPreference(self._requester, data) diff --git a/canvasapi/course.py b/canvasapi/course.py index aaf617e1..15e26c45 100644 --- a/canvasapi/course.py +++ b/canvasapi/course.py @@ -1,9 +1,13 @@ from canvasapi.canvas_object import CanvasObject from canvasapi.discussion_topic import DiscussionTopic from canvasapi.exceptions import RequiredFieldMissing +from canvasapi.folder import Folder from canvasapi.page import Page from canvasapi.paginated_list import PaginatedList +from canvasapi.tab import Tab +from canvasapi.submission import Submission from canvasapi.upload import Uploader +from canvasapi.user import UserDisplay from canvasapi.util import combine_kwargs @@ -944,6 +948,445 @@ def create_external_tool(self, name, privacy_level, consumer_key, shared_secret, return ExternalTool(self._requester, response_json) + def get_course_level_participation_data(self): + """ + Return page view hits and participation numbers grouped by day through the course's history + + :calls: `GET /api/v1/courses/:course_id/analytics/activity \ + `_ + + :rtype: dict + """ + + response = self._requester.request( + 'GET', + 'courses/%s/analytics/activity' % (self.id) + ) + + return response.json() + + def get_course_level_assignment_data(self, **kwargs): + """ + Return a list of assignments for the course sorted by due date + + :calls: `GET /api/v1/courses/:course_id/analytics/assignments \ + `_ + + :rtype: dict + """ + + response = self._requester.request( + 'GET', + 'courses/%s/analytics/assignments' % (self.id), + **combine_kwargs(**kwargs) + ) + + return response.json() + + def get_course_level_student_summary_data(self, **kwargs): + """ + Return a summary of per-user access information for all students in a course + + :calls: `GET /api/v1/courses/:course_id/analytics/student_summaries \ + `_ + + :rtype: dict + """ + + response = self._requester.request( + 'GET', + 'courses/%s/analytics/student_summaries' % (self.id), + **combine_kwargs(**kwargs) + ) + + return response.json() + + def get_user_in_a_course_level_participation_data(self, student_id): + """ + Return page view hits grouped by hour and participation details through course's history + + :calls: `GET /api/v1/courses/:course_id/analytics/users/:student_id/activity \ + `_ + + :rtype: dict + """ + + response = self._requester.request( + 'GET', + 'courses/%s/analytics/users/%s/activity' % (self.id, student_id) + ) + + return response.json() + + def get_user_in_a_course_level_assignment_data(self, student_id): + """ + Return a list of assignments for the course sorted by due date + + :calls: `GET /api/v1/courses/:course_id/analytics/users/:student_id/assignments \ + `_ + + :rtype: dict + """ + + response = self._requester.request( + 'GET', + 'courses/%s/analytics/users/%s/assignments' % (self.id, student_id) + ) + + return response.json() + + def get_user_in_a_course_level_messaging_data(self, student_id): + """ + Return messaging hits grouped by day through the entire history of the course + + :calls: `GET /api/v1/courses/:course_id/analytics/users/:student_id/communication \ + `_ + + :rtype: dict + """ + + response = self._requester.request( + 'GET', + 'courses/%s/analytics/users/%s/communication' % (self.id, student_id) + ) + + return response.json() + + def submit_assignment(self, assignment_id, submission, **kwargs): + """ + Makes a submission for an assignment. + + :calls: `POST /api/v1/courses/:course_id/assignments/:assignment_id/submissions \ + `_ + + :param submission: The attributes of the submission. + :type submission: `dict` + :rtype: :class:`canvasapi.submission.Submission` + """ + if isinstance(submission, dict) and 'submission_type' in submission: + kwargs['submision'] = submission + else: + raise RequiredFieldMissing( + "Dictionary with key 'submission_type' is required." + ) + + response = self._requester.request( + 'POST', + 'courses/%s/assignments/%s/submissions' % (self.id, assignment_id), + **combine_kwargs(**kwargs) + ) + + return Submission(self._requester, response.json()) + + def list_submissions(self, assignment_id, **kwargs): + """ + Makes a submission for an assignment. + + :calls: `GET /api/v1/courses/:course_id/assignments/:assignment_id/submissions \ + `_ + + :param assignment_id: The ID of the assignment. + :type assignment_id: `int` + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.submission.Submission` + """ + return PaginatedList( + Submission, + self._requester, + 'GET', + 'courses/%s/assignments/%s/submissions' % (self.id, assignment_id), + **combine_kwargs(**kwargs) + ) + + def list_multiple_submissions(self, **kwargs): + """ + List submissions for multiple assignments. + Get all existing submissions for a given set of students and assignments. + + :calls: `GET /api/v1/courses/:course_id/students/submissions \ + `_ + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.submission.Submission` + """ + return PaginatedList( + Submission, + self._requester, + 'GET', + 'courses/%s/students/submissions' % (self.id), + grouped=False, + **combine_kwargs(**kwargs) + ) + + def get_submission(self, assignment_id, user_id, **kwargs): + """ + Get a single submission, based on user id. + + :calls: `GET /api/v1/courses/:course_id/assignments/:assignment_id/submissions/:user_id \ + `_ + + :param assignment_id: The ID of the assignment. + :type assignment_id: int + :param user_id: The ID of the user. + :type user_id: str + :rtype: :class:`canvasapi.submission.Submission` + """ + response = self._requester.request( + 'GET', + 'courses/%s/assignments/%s/submissions/%s' % (self.id, assignment_id, user_id), + **combine_kwargs(**kwargs) + ) + return Submission(self._requester, response.json()) + + def update_submission(self, assignment_id, user_id, **kwargs): + """ + Comment on and/or update the grading for a student's assignment submission. + + :calls: `PUT /api/v1/courses/:course_id/assignments/:assignment_id/submissions/:user_id \ + `_ + + :param assignment_id: The ID of the assignment. + :type assignment_id: int + :param user_id: The ID of the user. + :type user_id: str + :rtype: :class:`canvasapi.submission.Submission` + """ + response = self._requester.request( + 'PUT', + 'courses/%s/assignments/%s/submissions/%s' % (self.id, assignment_id, user_id), + **combine_kwargs(**kwargs) + ) + + submission = self.get_submission(assignment_id, user_id) + + if 'submission_type' in response.json(): + super(Submission, submission).set_attributes(response.json()) + + return Submission(self._requester, response.json()) + + def list_gradeable_students(self, assignment_id): + """ + List students eligible to submit the assignment. + + :calls: `GET /api/v1/courses/:course_id/assignments/:assignment_id/gradeable_students \ + `_ + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.user.User` + """ + return PaginatedList( + UserDisplay, + self._requester, + 'GET', + 'courses/%s/assignments/%s/gradeable_students' % (self.id, assignment_id) + ) + + def mark_submission_as_read(self, assignment_id, user_id): + """ + Mark submission as read. No request fields are necessary. + + :calls: `PUT + /api/v1/courses/:course_id/assignments/:assignment_id/submissions/:user_id/read \ + `_ + + :rtype: `bool` + """ + response = self._requester.request( + 'PUT', + 'courses/%s/assignments/%s/submissions/%s/read' % ( + self.id, + assignment_id, + user_id, + ) + ) + return response.status_code == 204 + + def mark_submission_as_unread(self, assignment_id, user_id): + """ + Mark submission as unread. No request fields are necessary. + + :calls: `DELETE + /api/v1/courses/:course_id/assignments/:assignment_id/submissions/:user_id/read \ + `_ + + :rtype: `bool` + """ + response = self._requester.request( + 'DELETE', + 'courses/%s/assignments/%s/submissions/%s/read' % ( + self.id, + assignment_id, + user_id, + ), + ) + return response.status_code == 204 + + def list_external_feeds(self): + """ + Returns the list of External Feeds this course. + + :calls: `GET /api/v1/courses/:course_id/external_feeds \ + `_ + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.external_feed.ExternalFeed` + """ + from canvasapi.external_feed import ExternalFeed + return PaginatedList( + ExternalFeed, + self._requester, + 'GET', + 'courses/%s/external_feeds' % (self.id) + ) + + def create_external_feed(self, url, **kwargs): + """ + Create a new external feed for the course. + + :calls: `POST /api/v1/courses/:course_id/external_feeds \ + `_ + + :param url: The urlof the external rss or atom feed + :type url: str + :rtype: :class:`canvasapi.external_feed.ExternalFeed` + """ + from canvasapi.external_feed import ExternalFeed + response = self._requester.request( + 'POST', + 'courses/%s/external_feeds' % self.id, + url=url, + **combine_kwargs(**kwargs) + ) + return ExternalFeed(self._requester, response.json()) + + def delete_external_feed(self, feed_id): + """ + Deletes the external feed. + + :calls: `DELETE /api/v1/courses/:course_id/external_feeds/:external_feed_id \ + `_ + + :param feed_id: The id of the feed to be deleted. + :type feed_id: int + :rtype: :class:`canvasapi.external_feed.ExternalFeed` + """ + from canvasapi.external_feed import ExternalFeed + response = self._requester.request( + 'DELETE', + 'courses/%s/external_feeds/%s' % (self.id, feed_id) + ) + return ExternalFeed(self._requester, response.json()) + + def list_files(self, **kwargs): + """ + Returns the paginated list of files for the course. + + :calls: `GET api/v1/courses/:course_id/files \ + `_ + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.file.File` + """ + from canvasapi.file import File + + return PaginatedList( + File, + self._requester, + 'GET', + 'courses/%s/files' % (self.id), + **combine_kwargs(**kwargs) + ) + + def get_folder(self, folder_id): + """ + Returns the details for a course folder + + :calls: `GET /api/v1/courses/:course_id/folders/:id \ + `_ + + :param folder_id: The ID of the folder to retrieve. + :type folder_id: int + :rtype: :class:`canvasapi.folder.Folder` + """ + response = self._requester.request( + 'GET', + 'courses/%s/folders/%s' % (self.id, folder_id) + ) + return Folder(self._requester, response.json()) + + def list_folders(self): + """ + Returns the paginated list of all folders for the given course. This will be returned as a + flat list containing all subfolders as well. + + :calls: `GET /api/v1/courses/:course_id/folders \ + `_ + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.folder.Folder` + """ + return PaginatedList( + Folder, + self._requester, + 'GET', + 'courses/%s/folders' % (self.id) + ) + + def create_folder(self, name, **kwargs): + """ + Creates a folder in this course. + + :calls: `POST /api/v1/courses/:course_id/folders \ + `_ + + :param name: The name of the folder. + :type name: str + :rtype: :class:`canvasapi.folder.Folder` + """ + response = self._requester.request( + 'POST', + 'courses/%s/folders' % self.id, + name=name, + **combine_kwargs(**kwargs) + ) + return Folder(self._requester, response.json()) + + def list_tabs(self, **kwargs): + """ + List available tabs for a course. + Returns a list of navigation tabs available in the current context. + + :calls: `GET /api/v1/courses/:course_id/tabs \ + `_ + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.tab.Tab` + """ + return PaginatedList( + Tab, + self._requester, + 'GET', + 'courses/%s/tabs' % (self.id), + **combine_kwargs(**kwargs) + ) + + def update_tab(self, tab_id, **kwargs): + """ + Update a tab for a course. + + :calls: `PUT /api/v1/courses/:course_id/tabs/:tab_id \ + `_ + + :rtype: :class:`canvasapi.tab.Tab` + """ + response = self._requester.request( + 'PUT', + 'courses/%s/tabs/%s' % (self.id, tab_id), + **combine_kwargs(**kwargs) + ) + + return Tab(self._requester, response.json()) + class CourseNickname(CanvasObject): diff --git a/canvasapi/external_feed.py b/canvasapi/external_feed.py new file mode 100644 index 00000000..8769ba01 --- /dev/null +++ b/canvasapi/external_feed.py @@ -0,0 +1,7 @@ +from canvasapi.canvas_object import CanvasObject + + +class ExternalFeed(CanvasObject): + + def __str__(self): + return str(self.display_name) diff --git a/canvasapi/file.py b/canvasapi/file.py new file mode 100644 index 00000000..6b82c089 --- /dev/null +++ b/canvasapi/file.py @@ -0,0 +1,22 @@ +from canvasapi.canvas_object import CanvasObject + + +class File(CanvasObject): + + def __str__(self): + return str(self.display_name) + + def delete(self): + """ + Delete this file. + + :calls: `DELETE /api/v1/files/:id \ + `_ + + :rtype: :class:`canvasapi.file.File` + """ + response = self._requester.request( + 'DELETE', + 'files/%s' % (self.id) + ) + return File(self._requester, response.json()) diff --git a/canvasapi/folder.py b/canvasapi/folder.py new file mode 100644 index 00000000..a66adfba --- /dev/null +++ b/canvasapi/folder.py @@ -0,0 +1,102 @@ +from canvasapi.canvas_object import CanvasObject +from canvasapi.paginated_list import PaginatedList +from canvasapi.util import combine_kwargs + + +class Folder(CanvasObject): + + def __str__(self): + return str(self.full_name) + + def list_files(self, **kwargs): + """ + Returns the paginated list of files for the folder. + + :calls: `GET api/v1/folders/:id/files \ + `_ + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.file.File` + """ + from canvasapi.file import File + + return PaginatedList( + File, + self._requester, + 'GET', + 'folders/%s/files' % (self.id), + **combine_kwargs(**kwargs) + ) + + def delete(self, **kwargs): + """ + Remove this folder. You can only delete empty folders unless you set the + 'force' flag. + + :calls: `DELETE /api/v1/folders/:id \ + `_ + + :rtype: :class:`canvasapi.folder.Folder` + """ + response = self._requester.request( + 'DELETE', + 'folders/%s' % (self.id), + **combine_kwargs(**kwargs) + ) + return Folder(self._requester, response.json()) + + def list_folders(self): + """ + Returns the paginated list of folders in the folder. + + :calls: `GET /api/v1/folders/:id/folders \ + `_ + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.folder.Folder` + """ + return PaginatedList( + Folder, + self._requester, + 'GET', + 'folders/%s/folders' % (self.id) + ) + + def create_folder(self, name, **kwargs): + """ + Creates a folder within this folder. + + :calls: `POST /api/v1/folders/:folder_id/folders \ + `_ + + :param name: The name of the folder. + :type name: str + :rtype: :class:`canvasapi.folder.Folder` + """ + response = self._requester.request( + 'POST', + 'folders/%s/folders' % self.id, + name=name, + **combine_kwargs(**kwargs) + ) + return Folder(self._requester, response.json()) + + def update(self, **kwargs): + """ + Updates a folder. + + :calls: `PUT /api/v1/folders/:id \ + `_ + + :rtype: :class:`canvasapi.folder.Folder` + """ + response = self._requester.request( + 'PUT', + 'folders/%s' % self.id, + **combine_kwargs(**kwargs) + ) + + if 'name' in response.json(): + super(Folder, self).set_attributes(response.json()) + + return Folder(self._requester, response.json()) diff --git a/canvasapi/group.py b/canvasapi/group.py index 323479c0..22efa73d 100644 --- a/canvasapi/group.py +++ b/canvasapi/group.py @@ -1,7 +1,9 @@ from canvasapi.canvas_object import CanvasObject from canvasapi.discussion_topic import DiscussionTopic +from canvasapi.folder import Folder from canvasapi.exceptions import RequiredFieldMissing from canvasapi.paginated_list import PaginatedList +from canvasapi.tab import Tab from canvasapi.util import combine_kwargs @@ -463,6 +465,155 @@ def reorder_pinned_topics(self, order): return response.json().get('reorder') + def list_external_feeds(self): + """ + Returns the list of External Feeds this group. + + :calls: `GET /api/v1/groups/:group_id/external_feeds \ + `_ + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.external_feed.ExternalFeed` + """ + from canvasapi.external_feed import ExternalFeed + return PaginatedList( + ExternalFeed, + self._requester, + 'GET', + 'groups/%s/external_feeds' % (self.id) + ) + + def create_external_feed(self, url, **kwargs): + """ + Create a new external feed for the group. + + :calls: `POST /api/v1/groups/:group_id/external_feeds \ + `_ + + :param url: The urlof the external rss or atom feed + :type url: str + :rtype: :class:`canvasapi.external_feed.ExternalFeed` + """ + from canvasapi.external_feed import ExternalFeed + response = self._requester.request( + 'POST', + 'groups/%s/external_feeds' % self.id, + url=url, + **combine_kwargs(**kwargs) + ) + return ExternalFeed(self._requester, response.json()) + + def delete_external_feed(self, feed_id): + """ + Deletes the external feed. + + :calls: `DELETE /api/v1/groups/:group_id/external_feeds/:external_feed_id \ + `_ + + :param feed_id: The id of the feed to be deleted. + :type feed_id: int + :rtype: :class:`canvasapi.external_feed.ExternalFeed` + """ + from canvasapi.external_feed import ExternalFeed + response = self._requester.request( + 'DELETE', + 'groups/%s/external_feeds/%s' % (self.id, feed_id) + ) + return ExternalFeed(self._requester, response.json()) + + def list_files(self, **kwargs): + """ + Returns the paginated list of files for the group. + + :calls: `GET api/v1/courses/:group_id/files \ + `_ + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.file.File` + """ + from canvasapi.file import File + + return PaginatedList( + File, + self._requester, + 'GET', + 'groups/%s/files' % (self.id), + **combine_kwargs(**kwargs) + ) + + def get_folder(self, folder_id): + """ + Returns the details for a group's folder + + :calls: `GET /api/v1/groups/:group_id/folders/:id \ + `_ + + :param folder_id: The ID of the folder to retrieve. + :type folder_id: int + :rtype: :class:`canvasapi.folder.Folder` + """ + response = self._requester.request( + 'GET', + 'groups/%s/folders/%s' % (self.id, folder_id) + ) + return Folder(self._requester, response.json()) + + def list_folders(self): + """ + Returns the paginated list of all folders for the given group. This will be returned as a + flat list containing all subfolders as well. + + :calls: `GET /api/v1/groups/:group_id/folders \ + `_ + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.folder.Folder` + """ + return PaginatedList( + Folder, + self._requester, + 'GET', + 'groups/%s/folders' % (self.id) + ) + + def create_folder(self, name, **kwargs): + """ + Creates a folder in this group. + + :calls: `POST /api/v1/groups/:group_id/folders \ + `_ + + :param name: The name of the folder. + :type name: str + :rtype: :class:`canvasapi.folder.Folder` + """ + response = self._requester.request( + 'POST', + 'groups/%s/folders' % self.id, + name=name, + **combine_kwargs(**kwargs) + ) + return Folder(self._requester, response.json()) + + def list_tabs(self, **kwargs): + """ + List available tabs for a group. + Returns a list of navigation tabs available in the current context. + + :calls: `GET /api/v1/groups/:group_id/tabs \ + `_ + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.tab.Tab` + """ + return PaginatedList( + Tab, + self._requester, + 'GET', + 'groups/%s/tabs' % (self.id), + **combine_kwargs(**kwargs) + ) + class GroupMembership(CanvasObject): diff --git a/canvasapi/login.py b/canvasapi/login.py new file mode 100644 index 00000000..25cc22f8 --- /dev/null +++ b/canvasapi/login.py @@ -0,0 +1,41 @@ +from canvasapi.canvas_object import CanvasObject +from canvasapi.util import combine_kwargs + + +class Login(CanvasObject): + + def __str__(self): + return "{} ({})".format(self.id, self.unique_id) + + def delete(self): + """ + Delete an existing login. + + :calls: `DELETE /api/v1/users/:user_id/logins/:id \ + `_ + + :rtype: :class:`canvasapi.login.Login` + """ + + response = self._requester.request( + 'DELETE', + 'users/%s/logins/%s' % (self.id, self.unique_id) + ) + return Login(self._requester, response.json()) + + def edit(self, **kwargs): + """ + Update an existing login for a user in the given account. + + :calls: `PUT /api/v1/accounts/:account_id/logins/:id \ + `_ + + :rtype: :class:`canvasapi.login.Login` + """ + response = self._requester.request( + 'PUT', + 'accounts/%s/logins/%s' % (self.id, self.unique_id), + **combine_kwargs(**kwargs) + ) + + return Login(self._requester, response.json()) diff --git a/canvasapi/notification_preference.py b/canvasapi/notification_preference.py new file mode 100644 index 00000000..22c58f14 --- /dev/null +++ b/canvasapi/notification_preference.py @@ -0,0 +1,7 @@ +from canvasapi.canvas_object import CanvasObject + + +class NotificationPreference(CanvasObject): + + def __str__(self): + return "{} ({})".format(self.notification, self.frequency) diff --git a/canvasapi/section.py b/canvasapi/section.py index 2c8bc6dc..60609d0b 100644 --- a/canvasapi/section.py +++ b/canvasapi/section.py @@ -1,5 +1,7 @@ from canvasapi.canvas_object import CanvasObject +from canvasapi.exceptions import RequiredFieldMissing from canvasapi.paginated_list import PaginatedList +from canvasapi.submission import Submission from canvasapi.util import combine_kwargs @@ -92,3 +94,155 @@ def delete(self): "sections/%s" % (self.id) ) return Section(self._requester, response.json()) + + def submit_assignment(self, assignment_id, submission, **kwargs): + """ + Makes a submission for an assignment. + + :calls: `POST /api/v1/sections/:section_id/assignments/:assignment_id/submissions \ + `_ + + :param submission: The attributes of the submission. + :type submission: `dict` + :rtype: :class:`canvasapi.submission.Submission` + """ + if isinstance(submission, dict) and 'submission_type' in submission: + kwargs['submision'] = submission + else: + raise RequiredFieldMissing( + "Dictionary with key 'submission_type' is required." + ) + + response = self._requester.request( + 'POST', + 'sections/%s/assignments/%s/submissions' % (self.id, assignment_id), + **combine_kwargs(**kwargs) + ) + + return Submission(self._requester, response.json()) + + def list_submissions(self, assignment_id, **kwargs): + """ + Makes a submission for an assignment. + + :calls: `GET /api/v1/sections/:section_id/assignments/:assignment_id/submissions \ + `_ + + :param assignment_id: The ID of the assignment. + :type assignment_id: `int` + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.submission.Submission` + """ + return PaginatedList( + Submission, + self._requester, + 'GET', + 'sections/%s/assignments/%s/submissions' % (self.id, assignment_id), + **combine_kwargs(**kwargs) + ) + + def list_multiple_submissions(self, **kwargs): + """ + List submissions for multiple assignments. + Get all existing submissions for a given set of students and assignments. + + :calls: `GET /api/v1/sections/:section_id/students/submissions \ + `_ + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.submission.Submission` + """ + return PaginatedList( + Submission, + self._requester, + 'GET', + 'sections/%s/students/submissions' % (self.id), + grouped=False, + **combine_kwargs(**kwargs) + ) + + def get_submission(self, assignment_id, user_id, **kwargs): + """ + Get a single submission, based on user id. + + :calls: `GET /api/v1/sections/:section_id/assignments/:assignment_id/submissions/:user_id \ + `_ + + :param assignment_id: The ID of the assignment. + :type assignment_id: int + :param user_id: The ID of the user. + :type user_id: str + :rtype: :class:`canvasapi.submission.Submission` + """ + response = self._requester.request( + 'GET', + 'sections/%s/assignments/%s/submissions/%s' % (self.id, assignment_id, user_id), + **combine_kwargs(**kwargs) + ) + return Submission(self._requester, response.json()) + + def update_submission(self, assignment_id, user_id, **kwargs): + """ + Comment on and/or update the grading for a student's assignment submission. + + :calls: `PUT /api/v1/sections/:section_id/assignments/:assignment_id/submissions/:user_id \ + `_ + + :param assignment_id: The ID of the assignment. + :type assignment_id: int + :param user_id: The ID of the user. + :type user_id: str + :rtype: :class:`canvasapi.submission.Submission` + """ + response = self._requester.request( + 'PUT', + 'sections/%s/assignments/%s/submissions/%s' % (self.id, assignment_id, user_id), + **combine_kwargs(**kwargs) + ) + + submission = self.get_submission(assignment_id, user_id) + + if 'submission_type' in response.json(): + super(Submission, submission).set_attributes(response.json()) + + return Submission(self._requester, response.json()) + + def mark_submission_as_read(self, assignment_id, user_id): + """ + Mark submission as read. No request fields are necessary. + + :calls: `PUT + /api/v1/sections/:section_id/assignments/:assignment_id/submissions/:user_id/read \ + `_ + + :rtype: `bool` + """ + response = self._requester.request( + 'PUT', + 'sections/%s/assignments/%s/submissions/%s/read' % ( + self.id, + assignment_id, + user_id, + ) + ) + return response.status_code == 204 + + def mark_submission_as_unread(self, assignment_id, user_id): + """ + Mark submission as unread. No request fields are necessary. + + :calls: `DELETE + /api/v1/sections/:section_id/assignments/:assignment_id/submissions/:user_id/read \ + `_ + + :rtype: `bool` + """ + response = self._requester.request( + 'DELETE', + 'sections/%s/assignments/%s/submissions/%s/read' % ( + self.id, + assignment_id, + user_id, + ), + ) + return response.status_code == 204 diff --git a/canvasapi/submission.py b/canvasapi/submission.py new file mode 100644 index 00000000..27473c17 --- /dev/null +++ b/canvasapi/submission.py @@ -0,0 +1,7 @@ +from canvasapi.canvas_object import CanvasObject + + +class Submission(CanvasObject): + + def __str__(self): + return str(self.id) diff --git a/canvasapi/tab.py b/canvasapi/tab.py new file mode 100644 index 00000000..c35eaf5d --- /dev/null +++ b/canvasapi/tab.py @@ -0,0 +1,7 @@ +from canvasapi.canvas_object import CanvasObject + + +class Tab(CanvasObject): + + def __str__(self): + return "{} ({})".format(self.label, self.id) diff --git a/canvasapi/user.py b/canvasapi/user.py index 3a3c764b..97aaa918 100644 --- a/canvasapi/user.py +++ b/canvasapi/user.py @@ -1,6 +1,8 @@ from canvasapi.bookmark import Bookmark from canvasapi.calendar_event import CalendarEvent from canvasapi.canvas_object import CanvasObject +from canvasapi.communication_channel import CommunicationChannel +from canvasapi.folder import Folder from canvasapi.paginated_list import PaginatedList from canvasapi.upload import Uploader from canvasapi.util import combine_kwargs, obj_or_id @@ -321,6 +323,25 @@ def list_calendar_events_for_user(self, **kwargs): **combine_kwargs(**kwargs) ) + def list_communication_channels(self, **kwargs): + """ + List communication channels for the specified user, sorted by + position. + + :calls: `GET /api/v1/users/:user_id/communication_channels \ + `_ + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.communication_channel.CommunicationChannel` + """ + return PaginatedList( + CommunicationChannel, + self._requester, + 'GET', + 'users/%s/communication_channels' % (self.id), + **combine_kwargs(**kwargs) + ) + def list_bookmarks(self, **kwargs): """ List bookmarks that the current user can view or manage. @@ -380,6 +401,194 @@ def create_bookmark(self, name, url, **kwargs): **combine_kwargs(**kwargs) ) - vars(response.request) - return Bookmark(self._requester, response.json()) + + def list_files(self, **kwargs): + """ + Returns the paginated list of files for the user. + + :calls: `GET api/v1/courses/:user_id/files \ + `_ + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.file.File` + """ + from canvasapi.file import File + + return PaginatedList( + File, + self._requester, + 'GET', + 'users/%s/files' % (self.id), + **combine_kwargs(**kwargs) + ) + + def get_folder(self, folder_id): + """ + Returns the details for a user's folder + + :calls: `GET /api/v1/users/:user_id/folders/:id \ + `_ + + :param folder_id: The ID of the folder to retrieve. + :type folder_id: int + :rtype: :class:`canvasapi.folder.Folder` + """ + response = self._requester.request( + 'GET', + 'users/%s/folders/%s' % (self.id, folder_id) + ) + return Folder(self._requester, response.json()) + + def list_folders(self): + """ + Returns the paginated list of all folders for the given user. This will be returned as a + flat list containing all subfolders as well. + + :calls: `GET /api/v1/users/:user_id/folders \ + `_ + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.folder.Folder` + """ + return PaginatedList( + Folder, + self._requester, + 'GET', + 'users/%s/folders' % (self.id) + ) + + def create_folder(self, name, **kwargs): + """ + Creates a folder in this user. + + :calls: `POST /api/v1/users/:user_id/folders \ + `_ + + :param name: The name of the folder. + :type name: str + :rtype: :class:`canvasapi.folder.Folder` + """ + response = self._requester.request( + 'POST', + 'users/%s/folders' % self.id, + name=name, + **combine_kwargs(**kwargs) + ) + return Folder(self._requester, response.json()) + + def list_user_logins(self, **kwargs): + """ + Given a user ID, return that user's logins for the given account. + + :calls: `GET /api/v1/users/:user_id/logins \ + `_ + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.login.Login` + """ + from canvasapi.login import Login + + return PaginatedList( + Login, + self._requester, + 'GET', + 'users/%s/logins' % (self.id), + **combine_kwargs(**kwargs) + ) + + def list_observees(self, **kwargs): + """ + List the users that the given user is observing + + :calls: `GET /api/v1/users/:user_id/observees \ + `_ + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.user.User` + """ + + return PaginatedList( + User, + self._requester, + 'GET', + 'users/%s/observees' % (self.id), + **combine_kwargs(**kwargs) + ) + + def add_observee_with_credentials(self, **kwargs): + """ + Register the given user to observe another user, given the observee's credentials. + + :calls: `POST /api/v1/users/:user_id/observees \ + `_ + + :rtype: :class:`canvasapi.user.User` + """ + + response = self._requester.request( + 'POST', + 'users/%s/observees' % (self.id), + **combine_kwargs(**kwargs) + ) + return User(self._requester, response.json()) + + def show_observee(self, observee_id): + """ + Gets information about an observed user. + + :calls: `GET /api/v1/users/:user_id/observees/:observee_id \ + `_ + + :param unique_id: The login id for the user to observe. + :type observee: `dict` + :rtype: :class: `canvasapi.user.User` + """ + + response = self._requester.request( + 'GET', + 'users/%s/observees/%s' % (self.id, observee_id) + ) + return User(self._requester, response.json()) + + def add_observee(self, observee_id): + """ + Registers a user as being observed by the given user. + + :calls: `PUT /api/v1/users/:user_id/observees/:observee_id \ + `_ + + :param unique_id: The login id for the user to observe. + :type observee: `dict` + :rtype: :class: `canvasapi.user.User` + """ + + response = self._requester.request( + 'PUT', + 'users/%s/observees/%s' % (self.id, observee_id) + ) + return User(self._requester, response.json()) + + def remove_observee(self, observee_id): + """ + Unregisters a user as being observed by the given user. + + :calls: `DELETE /api/v1/users/:user_id/observees/:observee_id \ + `_ + + :param unique_id: The login id for the user to observe. + :type observee: `dict` + :rtype: :class: `canvasapi.user.User` + """ + + response = self._requester.request( + 'DELETE', + 'users/%s/observees/%s' % (self.id, observee_id) + ) + return User(self._requester, response.json()) + + +class UserDisplay(CanvasObject): + + def __str__(self): + return str(self.display_name) diff --git a/docs/account-ref.rst b/docs/account-ref.rst index 979f2144..71f1d44a 100644 --- a/docs/account-ref.rst +++ b/docs/account-ref.rst @@ -25,3 +25,10 @@ Role .. autoclass:: canvasapi.account.Role :members: + +=========== +SSOSettings +=========== + +.. autoclass:: canvasapi.account.SSOSettings + :members: \ No newline at end of file diff --git a/docs/authentication-provider-ref.rst b/docs/authentication-provider-ref.rst new file mode 100644 index 00000000..0b27aca9 --- /dev/null +++ b/docs/authentication-provider-ref.rst @@ -0,0 +1,6 @@ +====================== +AuthenticationProvider +====================== + +.. autoclass:: canvasapi.authentication_provider.AuthenticationProvider + :members: diff --git a/docs/class-reference.rst b/docs/class-reference.rst index 9f69138e..45991d3e 100644 --- a/docs/class-reference.rst +++ b/docs/class-reference.rst @@ -7,6 +7,7 @@ Class Reference account-ref appointment-group-ref assignment-ref + authentication-provider-ref avatar-ref bookmark-ref calendar-event-ref @@ -16,6 +17,7 @@ Class Reference enrollment-term-ref external-tool-ref group-ref + login-ref module-ref page-ref progress-ref diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 28f8363a..034afed2 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -1,23 +1,30 @@ -Getting Started with canvasapi +Getting Started with CanvasAPI =============================== -Installing canvasapi +Installing CanvasAPI --------------------- -You can install with pip:: +You can install CanvasAPI with pip:: pip install canvasapi Usage ----- -Before using canvasapi, you'll need to instantiate a new Canvas object: +Before using CanvasAPI, you'll need to instantiate a new Canvas object: .. code:: python + # Import the Canvas class from canvasapi import Canvas - canvas = Canvas(API_KEY, API_URL) + # Canvas API URL + API_URL = "https://example.com/api/v1/" + # Canvas API key + API_KEY = "p@$$w0rd" + + # Initialize a new Canvas object + canvas = Canvas(API_URL, API_KEY) You can now use :code:`canvas` to make API calls. diff --git a/docs/login-ref.rst b/docs/login-ref.rst new file mode 100644 index 00000000..9a3cf6ed --- /dev/null +++ b/docs/login-ref.rst @@ -0,0 +1,6 @@ +===== +Login +===== + +.. autoclass:: canvasapi.login.Login + :members: diff --git a/docs/user-ref.rst b/docs/user-ref.rst index 2f378bc9..b9668c9c 100644 --- a/docs/user-ref.rst +++ b/docs/user-ref.rst @@ -3,4 +3,4 @@ User ==== .. autoclass:: canvasapi.user.User - :members: \ No newline at end of file + :members: diff --git a/tests/fixtures/account.json b/tests/fixtures/account.json index b68c9e66..f8e04561 100644 --- a/tests/fixtures/account.json +++ b/tests/fixtures/account.json @@ -671,5 +671,323 @@ "label": "Student" }, "status_code": 200 + }, + "list_user_logins": { + "method": "GET", + "endpoint": "accounts/1/logins", + "data": [ + { + "account_id": 1, + "id": 2, + "sis_user_id": null, + "unique_id": "belieber@example.com", + "user_id": 2, + "authentication_provider_id": 1, + "authentication_provider_type": "facebook" + } + ], + "headers": { + "Link": "; rel=\"next\"" + }, + "status_code": 200 + }, + "list_user_logins_2": { + "method": "GET", + "endpoint": "accounts/1/logins/?page=2&per_page=2", + "data": [ + { + "account_id": 2, + "id": 3, + "sis_user_id": null, + "unique_id": "belieber@example.com", + "user_id": 3, + "authentication_provider_id": 2, + "authentication_provider_type": "facebook" + } + ], + "status_code": 200 + }, + "create_user_login": { + "method": "POST", + "endpoint": "accounts/1/logins", + "data": { + "id": 123, + "unique_id": 112233 + }, + "status_code": 200 + }, + "get_department_level_participation_data_with_given_term": { + "method": "GET", + "endpoint": "accounts/1/analytics/terms/1/activity", + "data": [ + { + "by_date": { + "2012-01-24": 1240, + "2012-01-27": 912 + }, + "by_category": { + "announcements": 54, + "assignments": 256, + "collaborations": 18, + "conferences": 26, + "discussions": 354, + "files": 132, + "general": 59, + "grades": 177, + "groups": 132, + "modules": 71, + "other": 412, + "pages": 105, + "quizzes": 356 + } + } + ] + }, + "get_department_level_participation_data_current": { + "method": "GET", + "endpoint": "accounts/1/analytics/current/activity", + "data": [ + { + "by_date": { + "2012-01-24": 1240, + "2012-01-27": 912 + }, + "by_category": { + "announcements": 54, + "assignments": 256, + "collaborations": 18, + "conferences": 26, + "discussions": 354, + "files": 132, + "general": 59, + "grades": 177, + "groups": 132, + "modules": 71, + "other": 412, + "pages": 105, + "quizzes": 356 + } + } + ] + }, + "get_department_level_participation_data_completed": { + "method": "GET", + "endpoint": "accounts/1/analytics/completed/activity", + "data": [ + { + "by_date": { + "2012-01-24": 1240, + "2012-01-27": 912 + }, + "by_category": { + "announcements": 54, + "assignments": 256, + "collaborations": 18, + "conferences": 26, + "discussions": 354, + "files": 132, + "general": 59, + "grades": 177, + "groups": 132, + "modules": 71, + "other": 412, + "pages": 105, + "quizzes": 356 + } + } + ] + }, + "get_department_level_grade_data_with_given_term": { + "method": "GET", + "endpoint": "accounts/1/analytics/terms/1/grades", + "data": [ + { + "0": "13435", + "1": "41", + "2": "58", + "3": "27", + "68": "1387", + "69": "1412", + "70": "2199", + "85": "5575", + "86": "6543", + "87": "6144", + "88": "7198", + "89": "6561", + "90": "8854", + "91": "7745", + "92": "8800", + "93": "7798" + } + ] + }, + "get_department_level_grade_data_current": { + "method": "GET", + "endpoint": "accounts/1/analytics/current/grades", + "data": [ + { + "0": "13435", + "1": "41", + "2": "58", + "3": "27", + "68": "1387", + "69": "1412", + "70": "2199", + "85": "5575", + "86": "6543", + "87": "6144", + "88": "7198", + "89": "6561", + "90": "8854", + "91": "7745", + "92": "8800", + "93": "7798" + } + ] + }, + "get_department_level_grade_data_completed": { + "method": "GET", + "endpoint": "accounts/1/analytics/completed/grades", + "data": [ + { + "0": "13435", + "1": "41", + "2": "58", + "3": "27", + "68": "1387", + "69": "1412", + "70": "2199", + "85": "5575", + "86": "6543", + "87": "6144", + "88": "7198", + "89": "6561", + "90": "8854", + "91": "7745", + "92": "8800", + "93": "7798" + } + ] + }, + "get_department_level_statistics_with_given_term": { + "method": "GET", + "endpoint": "accounts/1/analytics/terms/1/statistics", + "data": [ + { + "courses": 27, + "subaccounts": 3, + "teachers": 36, + "students": 418, + "discussion_topics": 77, + "media_objects": 219, + "attachments": 1268, + "assignments": 290 + } + ] + }, + "get_department_level_statistics_current": { + "method": "GET", + "endpoint": "accounts/1/analytics/current/statistics", + "data": [ + { + "courses": 27, + "subaccounts": 3, + "teachers": 36, + "students": 418, + "discussion_topics": 77, + "media_objects": 219, + "attachments": 1268, + "assignments": 290 + } + ] + }, + "get_department_level_statistics_completed": { + "method": "GET", + "endpoint": "accounts/1/analytics/completed/statistics", + "data": [ + { + "courses": 27, + "subaccounts": 3, + "teachers": 36, + "students": 418, + "discussion_topics": 77, + "media_objects": 219, + "attachments": 1268, + "assignments": 290 + } + ] + }, + "list_authentication_providers": { + "method": "GET", + "endpoint": "accounts/1/authentication_providers", + "data": [ + { + "id": 1, + "auth_type": "saml", + "position": 1 + }, + { + "id": 2, + "auth_type": "facebook", + "position": 1 + } + ], + "headers": { + "Link": "; rel=\"next\"" + }, + "status_code": 200 + }, + "list_authentication_providers_2": { + "method": "GET", + "endpoint": "accounts/1/authentication_providers/?page=2&per_page=2", + "data": [ + { + "id": 3, + "auth_type": "canvas", + "position": 1 + }, + { + "id": 4, + "auth_type": "microsoft", + "position": 1 + } + ], + "status_code": 200 + }, + "add_authentication_providers": { + "method": "POST", + "endpoint": "accounts/1/authentication_providers", + "data":{ + "id": 1, + "auth_type": "saml", + "position": 1 + }, + "status_code": 200 + }, + "get_authentication_providers": { + "method": "GET", + "endpoint": "accounts/1/authentication_providers/1", + "data": { + "id": 1, + "auth_type": "saml", + "position": 1 + }, + "status_code": 200 + }, + "show_account_auth_settings": { + "method": "GET", + "endpoint": "accounts/1/sso_settings", + "data": { + "login_handle_name": "Username", + "change_password_url": "https://example.com/reset_password" + } + }, + "update_account_auth_settings": { + "method": "PUT", + "endpoint": "accounts/1/sso_settings", + "data": { + "login_handle_name": "Username", + "change_password_url": "https://example.com/reset_password" + } } } diff --git a/tests/fixtures/authentication_providers.json b/tests/fixtures/authentication_providers.json new file mode 100644 index 00000000..446fae19 --- /dev/null +++ b/tests/fixtures/authentication_providers.json @@ -0,0 +1,69 @@ +{ + "list_authentication_providers": { + "method": "GET", + "endpoint": "accounts/1/authentication_providers", + "data": [ + { + "id": 1, + "auth_type": "twitter", + "position": 1 + }, + { + "id": 2, + "auth_type": "facebook", + "position": 1 + } + ], + "headers": { + "Link": "; rel=\"next\"" + }, + "status_code": 200 + }, + "list_authentication_providers_2": { + "method": "GET", + "endpoint": "accounts/1/authentication_providers/?page=2&per_page=2", + "data": [ + { + "id": 3, + "auth_type": "canvas", + "position": 1 + }, + { + "id": 4, + "auth_type": "microsoft", + "position": 1 + } + ], + "status_code": 200 + }, + "add_authentication_providers": { + "method": "POST", + "endpoint": "accounts/1/authentication_providers", + "data":{ + "id": 1, + "auth_type": "saml", + "position": 1 + }, + "status_code": 200 + }, + "update_authentication_providers": { + "method": "PUT", + "endpoint": "accounts/1/authentication_providers/1", + "data": { + "id": 1, + "auth_type": "New Authentication Providers", + "position": 1 + }, + "status_code": 200 + }, + "delete_authentication_providers": { + "method": "DELETE", + "endpoint": "accounts/1/authentication_providers/1", + "data": { + "id": 1, + "auth_type": "Authentication Providers", + "position": 1 + }, + "status_code": 200 + } +} \ No newline at end of file diff --git a/tests/fixtures/communication_channel.json b/tests/fixtures/communication_channel.json new file mode 100644 index 00000000..5e6a6890 --- /dev/null +++ b/tests/fixtures/communication_channel.json @@ -0,0 +1,46 @@ +{ + "list_preferences": { + "method": "GET", + "endpoint": "users/1/communication_channels/11/notification_preferences", + "data": { + "notification_preferences": [ + { + "frequency": "immediately", + "notification": "new_announcement", + "category": "announcement" + }, + { + "frequency": "weekly", + "notification": "assignment_due_date_changed", + "category": "due_date" + } + ] + }, + "status_code": 200 + }, + "get_preference": { + "method": "GET", + "endpoint": "users/1/communication_channels/11/notification_preferences/new_announcement", + "data": { + "notification_preferences": [ + { + "frequency": "immediately", + "notification": "new_announcement", + "category": "announcement" + } + ] + }, + "status_code": 200 + }, + "list_preference_categories": { + "method": "GET", + "endpoint": "users/1/communication_channels/11/notification_preference_categories", + "data": { + "categories": [ + "announcement", + "due_date" + ] + }, + "status_code": 200 + } +} diff --git a/tests/fixtures/course.json b/tests/fixtures/course.json index 6e8bf3db..93ff266b 100644 --- a/tests/fixtures/course.json +++ b/tests/fixtures/course.json @@ -7,6 +7,27 @@ }, "status_code": 200 }, + "create_external_feed": { + "method": "POST", + "endpoint": "courses/1/external_feeds", + "data": { + "id": 1, + "display_name": "My Blog", + "url": "http://example.com/myblog.rss" + }, + "status_code": 200 + }, + "create_folder": { + "method": "POST", + "endpoint": "courses/1/folders", + "data": { + "id": 2, + "name": "Test String", + "locked": false, + "hidden": false + }, + "status_code": 200 + }, "create_quiz": { "method": "POST", "endpoint": "courses/1/quizzes", @@ -35,6 +56,16 @@ }, "status_code": 200 }, + "delete_external_feed": { + "method": "DELETE", + "endpoint": "courses/1/external_feeds/1", + "data": { + "id": 1, + "display_name": "My Blog", + "url": "http://example.com/myblog.rss" + }, + "status_code": 200 + }, "enroll_user": { "method": "POST", "endpoint": "courses/1/enrollments", @@ -181,6 +212,18 @@ ], "status_code": 200 }, + "get_folder": { + "method": "GET", + "endpoint": "courses/1/folders/1", + "data": { + "id": 1, + "files_count": 10, + "folders_count": 2, + "name": "Folder 1", + "full_name": "Folder 1" + }, + "status_code": 200 + }, "get_quiz": { "method": "GET", "endpoint": "courses/1/quizzes/1", @@ -324,6 +367,81 @@ ], "status_code": 200 }, + "list_external_feeds": { + "method": "GET", + "endpoint": "courses/1/external_feeds", + "data": [ + { + "id": 1, + "display_name": "My Blog", + "url": "http://example.com/myblog.rss" + }, + { + "id": 2, + "display_name": "My Blog 2", + "url": "http://example.com/myblog2.rss" + } + ], + "status_code": 200 + }, + "list_course_files": { + "method": "GET", + "endpoint": "courses/1/files", + "data": [ + { + "id": 1, + "size": 2939, + "display_name": "File1.txt" + }, + { + "id": 2, + "size": 18380, + "display_name": "File_2.png" + } + ], + "status_code": 200, + "headers": { + "Link": "; rel=\"next\"" + } + }, + "list_course_files2": { + "method": "GET", + "endpoint": "courses/1/files?page=2&per_page=2", + "data": [ + { + "id": 3, + "display_name": "File 3.jpg", + "size": 1298 + }, + { + "id": 4, + "display_name": "File 4.docx", + "size": 88920 + } + ], + "status_code": 200 + }, + "list_folders": { + "method": "GET", + "endpoint": "courses/1/folders", + "data": [ + { + "id": 2, + "files_count": 0, + "folders_count": 0, + "name": "Folder 2", + "full_name": "course_files/Folder 2" + }, + { + "id": 3, + "files_count": 0, + "folders_count": 0, + "name": "Folder 3", + "full_name": "course_files/Folder 3" + } + ], + "status_code": 200 + }, "list_quizzes": { "method": "GET", "endpoint": "courses/1/quizzes", @@ -867,5 +985,338 @@ "order": "1, 2, 3" }, "status_code": 200 + }, + "get_course_level_participation_data": { + "method": "GET", + "endpoint": "courses/1/analytics/activity", + "data": [ + { + "date": "2012-01-24", + "participations": 3, + "views": 10 + } + ] + }, + "get_course_level_assignment_data": { + "method": "GET", + "endpoint": "courses/1/analytics/assignments", + "data": [ + { + "assignment_id": 1234, + "title": "Assignment 1", + "points_possible": 10, + "due_at": "2012-01-25T22:00:00-07:00", + "unlock_at": "2012-01-20T22:00:00-07:00", + "muted": false, + "min_score": 2, + "max_score": 10, + "median": 7, + "first_quartile": 4, + "third_quartile": 8, + "tardiness_breakdown": { + "on_time": 0.75, + "missing": 0.1, + "late": 0.15 + } + }, + { + "assignment_id": 1235, + "title": "Assignment 2", + "points_possible": 15, + "due_at": "2012-01-26T22:00:00-07:00", + "unlock_at": null, + "muted": true, + "min_score": 8, + "max_score": 8, + "median": 8, + "first_quartile": 8, + "third_quartile": 8, + "tardiness_breakdown": { + "on_time": 0.65, + "missing": 0.12, + "late": 0.23, + "total": 275 + } + } + ] + }, + "get_course_level_student_summary_data": { + "method": "GET", + "endpoint": "courses/1/analytics/student_summaries", + "data": [ + { + "id": 2346, + "page_views": 351, + "page_views_level": "1", + "max_page_view": 415, + "participations": 1, + "participations_level": "3", + "max_participations": 10, + "tardiness_breakdown": { + "total": 5, + "on_time": 3, + "late": 0, + "missing": 2, + "floating": 0 + } + }, + { + "id": 2345, + "page_views": 124, + "participations": 15, + "tardiness_breakdown": { + "total": 5, + "on_time": 1, + "late": 2, + "missing": 3, + "floating": 0 + } + } + ] + }, + "get_user_in_a_course_level_participation_data": { + "method": "GET", + "endpoint": "courses/1/analytics/users/1/activity", + "data": [ + { + "page_views": { + "2012-01-24T13:00:00-00:00": 19, + "2012-01-24T14:00:00-00:00": 13, + "2012-01-27T09:00:00-00:00": 23 + }, + "participations": [ + { + "created_at": "2012-01-21T22:00:00-06:00", + "url": "https://canvas.example.com/path/to/canvas" + }, + { + "created_at": "2012-01-27T22:00:00-06:00", + "url": "https://canvas.example.com/path/to/canvas" + } + ] + } + ] + }, + "get_user_in_a_course_level_assignment_data": { + "method": "GET", + "endpoint": "courses/1/analytics/users/1/assignments", + "data": [ + { + "assignment_id": 1234, + "title": "Assignment 1", + "points_possible": 10, + "due_at": "2012-01-25T22:00:00-07:00", + "unlock_at": "2012-01-20T22:00:00-07:00", + "muted": false, + "min_score": 2, + "max_score": 10, + "median": 7, + "first_quartile": 4, + "third_quartile": 8, + "module_ids": [ + 1, + 2 + ], + "submission": { + "submitted_at": "2012-01-22T22:00:00-07:00", + "score": 10 + } + }, + { + "assignment_id": 1235, + "title": "Assignment 2", + "points_possible": 15, + "due_at": "2012-01-26T22:00:00-07:00", + "unlock_at": null, + "muted": true, + "min_score": 8, + "max_score": 8, + "median": 8, + "first_quartile": 8, + "third_quartile": 8, + "module_ids": [ + 1 + ], + "submission": { + "submitted_at": "2012-01-22T22:00:00-07:00" + } + } + ] + }, + "get_user_in_a_course_level_messaging_data": { + "method": "GET", + "endpoint": "courses/1/analytics/users/1/communication", + "data": [ + { + "2012-01-24":{ + "instructorMessages":1, + "studentMessages":2 + }, + "2012-01-27":{ + "studentMessages":1 + } + } + ] + }, + "list_tabs": { + "method": "GET", + "endpoint": "courses/1/tabs", + "data": [ + { + "id": "home", + "html_url": "/courses/1", + "position": 1, + "visibility": "public", + "label": "Home", + "type": "internal" + }, + { + "id": "pages", + "html_url": "/courses/1/wiki", + "position": 2, + "visibility": "public", + "label": "Pages", + "type": "internal" + } + ], + "status_code": 200 + }, + "submit_assignment": { + "method": "POST", + "endpoint": "courses/1/assignments/1/submissions", + "data": { + "id": 1, + "assignment_id": 1, + "user_id": 1, + "html_url": "http://example.com/courses/1/assignments/1/submissions/1", + "submission_type": "online_upload" + }, + "status_code": 200 + }, + "list_submissions": { + "method": "GET", + "endpoint": "courses/1/assignments/1/submissions", + "data": [ + { + "id": 1, + "assignments_id": 1, + "user_id": 1, + "html_url": "http://example.com/courses/1/assignments/1/submissions/1", + "submission_type": "online_upload" + }, + { + "id": 2, + "assignments_id": 1, + "user_id": 2, + "html_url": "http://example.com/courses/1/assignments/1/submissions/2", + "submission_type": "online_upload" + } + ], + "status_code": 200 + }, + "list_multiple_submissions": { + "method": "GET", + "endpoint": "courses/1/students/submissions", + "data": [ + { + "id": 1, + "assignments_id": 1, + "user_id": 1, + "html_url": "http://example.com/courses/1/assignments/1/submissions/1", + "submission_type": "online_upload" + }, + { + "id": 2, + "assignments_id": 1, + "user_id": 2, + "html_url": "http://example.com/courses/1/assignments/1/submissions/2", + "submission_type": "online_upload" + } + ], + "status_code": 200 + }, + "get_submission": { + "method": "GET", + "endpoint": "courses/1/assignments/1/submissions/1", + "data": { + "id": 1, + "assignments_id": 1, + "user_id": 1, + "html_url": "http://example.com/courses/1/assignments/1/submissions/1", + "submission_type": "online_upload" + }, + "status_code": 200 + }, + "list_gradeable_students": { + "method": "GET", + "endpoint": "courses/1/assignments/1/gradeable_students", + "data": [ + { + "id": 1, + "display_name": "Student 1" + }, + { + "id": 2, + "display_name": "Student 2" + } + ], + "status_code": 200 + }, + "update_tab": { + "method": "PUT", + "endpoint": "courses/1/tabs/pages", + "data": { + "id": "pages", + "html_url": "/courses/1/wiki", + "position": 3, + "visibility": "public", + "label": "Pages", + "type": "internal" + }, + "status_code": 200 + }, + "update_submission": { + "method": "PUT", + "endpoint": "courses/1/assignments/1/submissions/1", + "data": { + "id": 1, + "assignments_id": 1, + "user_id": 1, + "html_url": "http://example.com/courses/1/assignments/1/submissions/1", + "submission_type": "online_upload", + "excused": true + }, + "status_code": 200 + }, + "mark_submission_as_read": { + "method": "PUT", + "endpoint": "courses/1/assignments/1/submissions/1/read", + "status_code": 204 + }, + "mark_submission_as_unread": { + "method": "DELETE", + "endpoint": "courses/1/assignments/1/submissions/1/read", + "status_code": 204 + }, + "search_all_courses": { + "method": "GET", + "endpoint": "search/all_courses", + "data": [ + { + "course": + { + "id": 1, + "name": "Course 1" + } + }, + { + "course": + { + "id": 2, + "name": "Course 2" + } + } + ], + "status_code": 200 } } diff --git a/tests/fixtures/file.json b/tests/fixtures/file.json new file mode 100644 index 00000000..23a0b969 --- /dev/null +++ b/tests/fixtures/file.json @@ -0,0 +1,12 @@ +{ + "delete_file": { + "method": "DELETE", + "endpoint": "files/1", + "data": { + "id": 1, + "display_name": "Bad File.docx", + "size": 5512 + }, + "status_code": 200 + } +} diff --git a/tests/fixtures/folder.json b/tests/fixtures/folder.json new file mode 100644 index 00000000..a667019e --- /dev/null +++ b/tests/fixtures/folder.json @@ -0,0 +1,107 @@ +{ + "create_folder": { + "method": "POST", + "endpoint": "folders/1/folders", + "data": { + "id": 2, + "name": "Test String", + "locked": false, + "hidden": false + }, + "status_code": 200 + }, + "get_by_id": { + "method": "GET", + "endpoint": "folders/1", + "data": { + "id": 1, + "files_count": 10, + "folders_count": 2, + "name": "Folder 1", + "full_name": "course_files/Folder 1" + }, + "status_code": 200 + }, + "list_folder_files": { + "method": "GET", + "endpoint": "folders/1/files", + "data": [ + { + "id": 1, + "size": 2939, + "display_name": "File1.txt" + }, + { + "id": 2, + "size": 18380, + "display_name": "File_2.png" + } + ], + "status_code": 200, + "headers": { + "Link": "; rel=\"next\"" + } + }, + "list_folder_files2": { + "method": "GET", + "endpoint": "folders/1/files?page=2&per_page=2", + "data": [ + { + "id": 3, + "display_name": "File 3.jpg", + "size": 1298 + }, + { + "id": 4, + "display_name": "File 4.docx", + "size": 88920 + } + ], + "status_code": 200 + }, + "delete_folder": { + "method": "DELETE", + "endpoint": "folders/1", + "data": { + "id": 1, + "files_count": 0, + "folders_count": 0, + "name": "Folder 1", + "full_name": "course_files/Folder 1" + }, + "status_code": 200 + }, + "list_folders": { + "method": "GET", + "endpoint": "folders/1/folders", + "data": [ + { + "id": 2, + "files_count": 0, + "folders_count": 0, + "name": "Folder 2", + "full_name": "course_files/Folder 2" + }, + { + "id": 3, + "files_count": 0, + "folders_count": 0, + "name": "Folder 3", + "full_name": "course_files/Folder 3" + } + ], + "status_code": 200 + }, + "update": { + "method": "PUT", + "endpoint": "folders/1", + "data": { + "id": 1, + "files_count": 0, + "folders_count": 0, + "name": "New Name", + "full_name": "course_files/New Name" + }, + "status_code": 200 + } +} diff --git a/tests/fixtures/group.json b/tests/fixtures/group.json index 17ac9dd7..f8811927 100644 --- a/tests/fixtures/group.json +++ b/tests/fixtures/group.json @@ -29,6 +29,17 @@ }, "status_code": 200 }, + "create_folder": { + "method": "POST", + "endpoint": "groups/1/folders", + "data": { + "id": 2, + "name": "Test String", + "locked": false, + "hidden": false + }, + "status_code": 200 + }, "create_page": { "method": "POST", "endpoint": "groups/1/pages", @@ -655,5 +666,135 @@ "order": "1, 2, 3" }, "status_code": 200 + }, + "delete_external_feed": { + "method": "DELETE", + "endpoint": "groups/1/external_feeds/1", + "data": { + "id": 1, + "display_name": "My Blog", + "url": "http://example.com/myblog.rss" + }, + "status_code": 200 + }, + "list_external_feeds": { + "method": "GET", + "endpoint": "groups/1/external_feeds", + "data": [ + { + "id": 1, + "display_name": "My Blog", + "url": "http://example.com/myblog.rss" + }, + { + "id": 2, + "display_name": "My Blog 2", + "url": "http://example.com/myblog2.rss" + } + ], + "status_code": 200 + }, + "create_external_feed": { + "method": "POST", + "endpoint": "groups/1/external_feeds", + "data": { + "id": 1, + "display_name": "My Blog", + "url": "http://example.com/myblog.rss" + }, + "status_code": 200 + }, + "list_group_files": { + "method": "GET", + "endpoint": "groups/1/files", + "data": [ + { + "id": 1, + "size": 2939, + "display_name": "File1.txt" + }, + { + "id": 2, + "size": 18380, + "display_name": "File_2.png" + } + ], + "status_code": 200, + "headers": { + "Link": "; rel=\"next\"" + } + }, + "list_group_files2": { + "method": "GET", + "endpoint": "groups/1/files?page=2&per_page=2", + "data": [ + { + "id": 3, + "display_name": "File 3.jpg", + "size": 1298 + }, + { + "id": 4, + "display_name": "File 4.docx", + "size": 88920 + } + ], + "status_code": 200 + }, + "get_folder": { + "method": "GET", + "endpoint": "groups/1/folders/1", + "data": { + "id": 1, + "files_count": 10, + "folders_count": 2, + "name": "Folder 1", + "full_name": "Folder 1" + }, + "status_code": 200 + }, + "list_folders": { + "method": "GET", + "endpoint": "groups/1/folders", + "data": [ + { + "id": 2, + "files_count": 0, + "folders_count": 0, + "name": "Folder 2", + "full_name": "group_files/Folder 2" + }, + { + "id": 3, + "files_count": 0, + "folders_count": 0, + "name": "Folder 3", + "full_name": "group_files/Folder 3" + } + ], + "status_code": 200 + }, + "list_tabs": { + "method": "GET", + "endpoint": "groups/1/tabs", + "data": [ + { + "id": "home", + "html_url": "/groups/1", + "position": 1, + "visibility": "public", + "label": "Home", + "type": "internal" + }, + { + "id": "pages", + "html_url": "/groups/1/wiki", + "position": 2, + "visibility": "public", + "label": "Pages", + "type": "internal" + } + ], + "status_code": 200 } } diff --git a/tests/fixtures/login.json b/tests/fixtures/login.json new file mode 100644 index 00000000..b0959f4a --- /dev/null +++ b/tests/fixtures/login.json @@ -0,0 +1,63 @@ +{ + "create_user_login": { + "method": "POST", + "endpoint": "accounts/1/logins", + "data": { + "id": 123, + "unique_id": 112233 + }, + "status_code": 200 + }, + "list_user_logins": { + "method": "GET", + "endpoint": "accounts/1/logins", + "data": [ + { + "account_id": 1, + "id": 2, + "sis_user_id": null, + "unique_id": "belieber@example.com", + "user_id": 2, + "authentication_provider_id": 1, + "authentication_provider_type": "facebook" + } + ], + "headers": { + "Link": "; rel=\"next\"" + }, + "status_code": 200 + }, + "list_user_logins_2": { + "method": "GET", + "endpoint": "accounts/1/logins/?page=2&per_page=2", + "data": [ + { + "account_id": 2, + "id": 3, + "sis_user_id": null, + "unique_id": "belieber@example.com", + "user_id": 3, + "authentication_provider_id": 2, + "authentication_provider_type": "facebook" + } + ], + "status_code": 200 + }, + "edit_user_login": { + "method": "PUT", + "endpoint": "accounts/123/logins/112233", + "data": { + "id": 123, + "unique_id": 112233 + }, + "status_code": 200 + }, + "delete_user_login": { + "method": "DELETE", + "endpoint": "users/123/logins/112233", + "data": { + "id": 123, + "unique_id": 112233 + } + } +} diff --git a/tests/fixtures/section.json b/tests/fixtures/section.json index 93d0fb9b..6d06bcd8 100644 --- a/tests/fixtures/section.json +++ b/tests/fixtures/section.json @@ -82,5 +82,94 @@ "name": "Deleted Section" }, "status_code": 200 + }, + "submit_assignment": { + "method": "POST", + "endpoint": "sections/1/assignments/1/submissions", + "data": { + "id": 1, + "assignment_id": 1, + "user_id": 1, + "html_url": "http://example.com/sections/1/assignments/1/submissions/1", + "submission_type": "online_upload" + }, + "status_code": 200 + }, + "list_submissions": { + "method": "GET", + "endpoint": "sections/1/assignments/1/submissions", + "data": [ + { + "id": 1, + "assignments_id": 1, + "user_id": 1, + "html_url": "http://example.com/sections/1/assignments/1/submissions/1", + "submission_type": "online_upload" + }, + { + "id": 2, + "assignments_id": 1, + "user_id": 2, + "html_url": "http://example.com/sections/1/assignments/1/submissions/2", + "submission_type": "online_upload" + } + ], + "status_code": 200 + }, + "list_multiple_submissions": { + "method": "GET", + "endpoint": "sections/1/students/submissions", + "data": [ + { + "id": 1, + "assignments_id": 1, + "user_id": 1, + "html_url": "http://example.com/sections/1/assignments/1/submissions/1", + "submission_type": "online_upload" + }, + { + "id": 2, + "assignments_id": 1, + "user_id": 2, + "html_url": "http://example.com/sections/1/assignments/1/submissions/2", + "submission_type": "online_upload" + } + ], + "status_code": 200 + }, + "get_submission": { + "method": "GET", + "endpoint": "sections/1/assignments/1/submissions/1", + "data": { + "id": 1, + "assignments_id": 1, + "user_id": 1, + "html_url": "http://example.com/sections/1/assignments/1/submissions/1", + "submission_type": "online_upload" + }, + "status_code": 200 + }, + "update_submission": { + "method": "PUT", + "endpoint": "sections/1/assignments/1/submissions/1", + "data": { + "id": 1, + "assignments_id": 1, + "user_id": 1, + "html_url": "http://example.com/sections/1/assignments/1/submissions/1", + "submission_type": "online_upload", + "excused": true + }, + "status_code": 200 + }, + "mark_submission_as_read": { + "method": "PUT", + "endpoint": "sections/1/assignments/1/submissions/1/read", + "status_code": 204 + }, + "mark_submission_as_unread": { + "method": "DELETE", + "endpoint": "sections/1/assignments/1/submissions/1/read", + "status_code": 204 } -} \ No newline at end of file +} diff --git a/tests/fixtures/user.json b/tests/fixtures/user.json index 6ad0dab3..fabf08bb 100644 --- a/tests/fixtures/user.json +++ b/tests/fixtures/user.json @@ -182,6 +182,17 @@ ], "status_code": 200 }, + "create_folder": { + "method": "POST", + "endpoint": "users/1/folders", + "data": { + "id": 2, + "name": "Test String", + "locked": false, + "hidden": false + }, + "status_code": 200 + }, "edit": { "method": "PUT", "endpoint": "users/1", @@ -215,6 +226,18 @@ "name": "John Doe" } }, + "get_folder": { + "method": "GET", + "endpoint": "users/1/folders/1", + "data": { + "id": 1, + "files_count": 10, + "folders_count": 2, + "name": "Folder 1", + "full_name": "Folder 1" + }, + "status_code": 200 + }, "get_user_assignments": { "method": "GET", "endpoint": "users/1/courses/1/assignments", @@ -256,6 +279,43 @@ ], "status_code": 200 }, + "get_user_files": { + "method": "GET", + "endpoint": "users/1/files", + "data": [ + { + "id": 1, + "size": 2939, + "display_name": "File1.txt" + }, + { + "id": 2, + "size": 18380, + "display_name": "File_2.png" + } + ], + "status_code": 200, + "headers": { + "Link": "; rel=\"next\"" + } + }, + "get_user_files2": { + "method": "GET", + "endpoint": "users/1/files?page=2&per_page=2", + "data": [ + { + "id": 3, + "display_name": "File 3.jpg", + "size": 1298 + }, + { + "id": 4, + "display_name": "File 4.docx", + "size": 88920 + } + ], + "status_code": 200 + }, "list_calendar_events_for_user": { "method": "GET", "endpoint": "users/1/calendar_events", @@ -273,6 +333,55 @@ ], "status_code": 200 }, + "list_comm_channels": { + "method": "GET", + "endpoint": "users/1/communication_channels", + "data": [ + { + "id": 11, + "address": "user@example.com", + "type": "email", + "position": 1, + "user_id": 1, + "workflow_state": "active" + }, + { + "id": 12, + "address": "user2@example.com", + "type": "email", + "position": 2, + "user_id": 1, + "workflow_state": "active" + } + ], + "status_code": 200, + "headers": { + "Link": "; rel=\"next\"" + } + }, + "list_comm_channels2": { + "method": "GET", + "endpoint": "users/1/communication_channels?page=2&per_page=2", + "data": [ + { + "id": 13, + "address": "5555555555@example.com", + "type": "sms", + "position": 3, + "user_id": 1, + "workflow_state": "active" + }, + { + "id": 14, + "address": "For All Devices", + "type": "push", + "position": 4, + "user_id": 1, + "workflow_state": "active" + } + ], + "status_code": 200 + }, "list_enrollments": { "method": "GET", "endpoint": "users/1/enrollments", @@ -306,6 +415,27 @@ ], "status_code": 200 }, + "list_folders": { + "method": "GET", + "endpoint": "users/1/folders", + "data": [ + { + "id": 2, + "files_count": 0, + "folders_count": 0, + "name": "Folder 2", + "full_name": "user_files/Folder 2" + }, + { + "id": 3, + "files_count": 0, + "folders_count": 0, + "name": "Folder 3", + "full_name": "user_files/Folder 3" + } + ], + "status_code": 200 + }, "list_groups": { "method": "GET", "endpoint": "users/self/groups", @@ -506,5 +636,124 @@ "data": { "url": "great_url_success" } + }, + "list_user_logins": { + "method": "GET", + "endpoint": "users/1/logins", + "data": [ + { + "account_id": 1, + "id": 2, + "sis_user_id": null, + "unique_id": "belieber@example.com", + "user_id": 2, + "authentication_provider_id": 1, + "authentication_provider_type": "facebook" + } + ], + "headers": { + "Link": "; rel=\"next\"" + }, + "status_code": 200 + }, + "list_user_logins_2": { + "method": "GET", + "endpoint": "users/1/logins/?page=2&per_page=2", + "data": [ + { + "account_id": 2, + "id": 3, + "sis_user_id": null, + "unique_id": "belieber@example.com", + "user_id": 3, + "authentication_provider_id": 2, + "authentication_provider_type": "facebook" + } + ], + "status_code": 200 + }, + "list_observees": { + "method": "GET", + "endpoint": "users/1/observees", + "data": [ + { + "id": 1, + "name": "User 1" + }, + { + "id": 2, + "name": "User 2" + } + ], + "headers": { + "Link": "; rel=\"next\"" + }, + "status_code": 200 + }, + "list_observees_2": { + "method": "GET", + "endpoint": "users/1/observees?page=2&per_page=2", + "data": [ + { + "id": 3, + "name": "User 3" + }, + { + "id": 4, + "name": "User 4" + } + ], + "status_code": 200 + }, + "add_observee_with_credentials": { + "method": "POST", + "endpoint": "users/1/observees", + "data": { + "id": 5, + "name": "User 5" + } + }, + "show_observee": { + "method": "GET", + "endpoint": "users/1/observees/6", + "data": { + "id": 6, + "name": "User 6" + } + }, + "add_observee": { + "method": "PUT", + "endpoint": "users/1/observees/7", + "data": { + "id": 7, + "name": "User 7" + } + }, + "remove_observee": { + "method": "DELETE", + "endpoint": "users/1/observees/8", + "data": { + "id": 8, + "name": "User 8" + } + }, + "search_recipients": { + "method": "GET", + "endpoint": "search/recipients", + "data": [ + { + "id": "group_1", + "name": "the group", + "type": "context", + "user_count": 3 + }, + { + "id": 2, + "name": "greg", + "common_courses": {}, + "common_groups": {"1": ["Member"]} + } + ], + "status_code": 200 } } diff --git a/tests/test_account.py b/tests/test_account.py index eb72c656..c296f4ea 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -4,7 +4,7 @@ import requests_mock from canvasapi import Canvas -from canvasapi.account import Account, AccountNotification, AccountReport, Role +from canvasapi.account import Account, AccountNotification, AccountReport, Role, SSOSettings from canvasapi.course import Course from canvasapi.enrollment import Enrollment from canvasapi.enrollment_term import EnrollmentTerm @@ -12,6 +12,8 @@ from canvasapi.exceptions import RequiredFieldMissing from canvasapi.group import Group, GroupCategory from canvasapi.user import User +from canvasapi.login import Login +from canvasapi.authentication_provider import AuthenticationProvider from tests import settings from tests.util import register_uris @@ -396,3 +398,156 @@ def test_list_enrollment_terms(self, m): enrollment_terms_list = [category for category in response] self.assertIsInstance(enrollment_terms_list[0], EnrollmentTerm) + + # list_user_logins() + def test_list_user_logins(self, m): + requires = {'account': ['list_user_logins', 'list_user_logins_2']} + register_uris(requires, m) + + response = self.account.list_user_logins() + login_list = [login for login in response] + + self.assertIsInstance(login_list[0], Login) + self.assertEqual(len(login_list), 2) + + # create_user_login() + def test_create_user_login(self, m): + register_uris({'account': ['create_user_login']}, m) + + response = self.account.create_user_login(user={'id': 123}, login={'unique_id': 112233}) + + self.assertIsInstance(response, Login) + self.assertTrue(hasattr(response, 'id')) + self.assertTrue(hasattr(response, 'unique_id')) + self.assertEqual(response.id, 123) + self.assertEqual(response.unique_id, 112233) + + def test_create_user_login_fail_on_user_id(self, m): + with self.assertRaises(RequiredFieldMissing): + self.account.create_user_login(user={}, login={}) + + def test_create_user_login_fail_on_login_unique_id(self, m): + with self.assertRaises(RequiredFieldMissing): + self.account.create_user_login(user={'id': 123}, login={}) + + # get_department_level_participation_data_with_given_term() + def test_get_department_level_participation_data_with_given_term(self, m): + register_uris({'account': ['get_department_level_participation_data_with_given_term']}, m) + + response = self.account.get_department_level_participation_data_with_given_term(1) + + self.assertIsInstance(response, list) + + # get_department_level_participation_data_current() + def test_get_department_level_participation_data_current(self, m): + register_uris({'account': ['get_department_level_participation_data_current']}, m) + + response = self.account.get_department_level_participation_data_current() + + self.assertIsInstance(response, list) + + # get_department_level_participation_data_completed() + def test_get_department_level_participation_data_completed(self, m): + register_uris({'account': ['get_department_level_participation_data_completed']}, m) + + response = self.account.get_department_level_participation_data_completed() + + self.assertIsInstance(response, list) + + # get_department_level_grade_data_with_given_term() + def test_get_department_level_grade_data_with_given_term(self, m): + register_uris({'account': ['get_department_level_grade_data_with_given_term']}, m) + + response = self.account.get_department_level_grade_data_with_given_term(1) + + self.assertIsInstance(response, list) + + # get_department_level_grade_data_current() + def test_get_department_level_grade_data_current(self, m): + register_uris({'account': ['get_department_level_grade_data_current']}, m) + + response = self.account.get_department_level_grade_data_current() + + self.assertIsInstance(response, list) + + # get_department_level_grade_data_completed() + def test_get_department_level_grade_data_completed(self, m): + register_uris({'account': ['get_department_level_grade_data_completed']}, m) + + response = self.account.get_department_level_grade_data_completed() + + self.assertIsInstance(response, list) + + # get_department_level_statistics_with_given_term() + def test_get_department_level_statistics_with_given_term(self, m): + register_uris({'account': ['get_department_level_statistics_with_given_term']}, m) + + response = self.account.get_department_level_statistics_with_given_term(1) + + self.assertIsInstance(response, list) + + # get_department_level_statistics_current() + def test_get_department_level_statistics_current(self, m): + register_uris({'account': ['get_department_level_statistics_current']}, m) + + response = self.account.get_department_level_statistics_current() + + self.assertIsInstance(response, list) + + # get_department_level_statistics_completed() + def test_get_department_level_statistics_completed(self, m): + register_uris({'account': ['get_department_level_statistics_completed']}, m) + + response = self.account.get_department_level_statistics_completed() + + self.assertIsInstance(response, list) + + # list_authentication_providers() + def test_list_authentication_providers(self, m): + requires = {'account': ['list_authentication_providers', + 'list_authentication_providers_2']} + register_uris(requires, m) + + authentication_providers = self.account.list_authentication_providers() + authentication_providers_list = [ + authentication_provider for authentication_provider in authentication_providers + ] + + self.assertEqual(len(authentication_providers_list), 4) + self.assertIsInstance(authentication_providers_list[0], AuthenticationProvider) + self.assertTrue(hasattr(authentication_providers_list[0], 'auth_type')) + self.assertTrue(hasattr(authentication_providers_list[0], 'position')) + + # add_authentication_providers() + def test_add_authentication_providers(self, m): + register_uris({'account': ['add_authentication_providers']}, m) + + new_authentication_provider = self.account.add_authentication_providers() + + self.assertIsInstance(new_authentication_provider, AuthenticationProvider) + self.assertTrue(hasattr(new_authentication_provider, 'auth_type')) + self.assertTrue(hasattr(new_authentication_provider, 'position')) + + # get_authentication_providers() + def test_get_authentication_providers(self, m): + register_uris({'account': ['get_authentication_providers']}, m) + + response = self.account.get_authentication_providers(1) + + self.assertIsInstance(response, AuthenticationProvider) + + # show_account_auth_settings() + def test_show_account_auth_settings(self, m): + register_uris({'account': ['show_account_auth_settings']}, m) + + response = self.account.show_account_auth_settings() + + self.assertIsInstance(response, SSOSettings) + + # update_account_auth_settings() + def test_update_account_auth_settings(self, m): + register_uris({'account': ['update_account_auth_settings']}, m) + + response = self.account.update_account_auth_settings() + + self.assertIsInstance(response, SSOSettings) diff --git a/tests/test_authentication_providers.py b/tests/test_authentication_providers.py new file mode 100644 index 00000000..6e60a4a8 --- /dev/null +++ b/tests/test_authentication_providers.py @@ -0,0 +1,52 @@ +import unittest + +import requests_mock + +from canvasapi import Canvas +from canvasapi.authentication_provider import AuthenticationProvider +from tests import settings +from tests.util import register_uris + + +@requests_mock.Mocker() +class TestAuthenticationProvider(unittest.TestCase): + + @classmethod + def setUp(self): + self.canvas = Canvas(settings.BASE_URL, settings.API_KEY) + + with requests_mock.Mocker() as m: + register_uris({ + 'account': ['get_by_id', 'add_authentication_providers'] + }, m) + + self.account = self.canvas.get_account(1) + self.authentication_providers = self.account.add_authentication_providers( + authentication_providers={ + "auth_type": "Authentication Providers" + } + ) + + # update() + def test_update_authentication_providers(self, m): + register_uris({'authentication_providers': ['update_authentication_providers']}, m) + + new_auth_type = 'New Authentication Providers' + + self.authentication_providers.update(authentication_providers={"auth_type": new_auth_type}) + self.assertEqual(self.authentication_providers.auth_type, new_auth_type) + + # delete() + def test_delete_authentication_providers(self, m): + register_uris({'authentication_providers': ['delete_authentication_providers']}, m) + + deleted_authentication_providers = self.authentication_providers.delete() + + self.assertIsInstance(deleted_authentication_providers, AuthenticationProvider) + self.assertTrue(hasattr(deleted_authentication_providers, 'auth_type')) + self.assertEqual(deleted_authentication_providers.auth_type, 'Authentication Providers') + + # __str__() + def test_str__(self, m): + string = str(self.authentication_providers) + self.assertIsInstance(string, str) diff --git a/tests/test_canvas.py b/tests/test_canvas.py index 58e7bc95..0be45707 100644 --- a/tests/test_canvas.py +++ b/tests/test_canvas.py @@ -443,3 +443,19 @@ def test_list_group_participants(self, m): groups = self.canvas.list_group_participants(222) groups_list = [group for group in groups] self.assertEqual(len(groups_list), 2) + + # search_recipients() + def test_search_recipients(self, m): + register_uris({'user': ['search_recipients']}, m) + + recipients = self.canvas.search_recipients() + self.assertIsInstance(recipients, list) + self.assertEqual(len(recipients), 2) + + # search_all_courses() + def test_search_all_courses(self, m): + register_uris({'course': ['search_all_courses']}, m) + + courses = self.canvas.search_all_courses() + self.assertIsInstance(courses, list) + self.assertEqual(len(courses), 2) diff --git a/tests/test_communication_channel.py b/tests/test_communication_channel.py new file mode 100644 index 00000000..ca28283f --- /dev/null +++ b/tests/test_communication_channel.py @@ -0,0 +1,56 @@ +import unittest + +import requests_mock + +from canvasapi import Canvas +from canvasapi.notification_preference import NotificationPreference +from tests import settings +from tests.util import register_uris + + +@requests_mock.Mocker() +class TestCommunicationChannel(unittest.TestCase): + + @classmethod + def setUp(self): + self.canvas = Canvas(settings.BASE_URL, settings.API_KEY) + + with requests_mock.Mocker() as m: + register_uris({'user': ['get_by_id', 'list_comm_channels']}, m) + + self.user = self.canvas.get_user(1) + self.comm_chan = self.user.list_communication_channels()[0] + + # __str__() + def test__str__(self, m): + string = str(self.comm_chan) + self.assertIsInstance(string, str) + + # list_preferences() + def test_list_preferences(self, m): + register_uris({'communication_channel': ['list_preferences']}, m) + + preferences = self.comm_chan.list_preferences() + preference_list = [preference for preference in preferences] + + self.assertEqual(len(preference_list), 2) + self.assertEqual(preference_list[0]['notification'], 'new_announcement') + + # list_preference_categories() + def test_list_preference_categories(self, m): + register_uris({'communication_channel': ['list_preference_categories']}, m) + + categories = self.comm_chan.list_preference_categories() + + self.assertEqual(len(categories), 2) + self.assertIsInstance(categories, list) + self.assertEqual(categories[0], 'announcement') + + # get_preference() + def test_get_preference(self, m): + register_uris({'communication_channel': ['get_preference']}, m) + + preference = self.comm_chan.get_preference('new_announcement') + self.assertIsInstance(preference, NotificationPreference) + self.assertTrue(hasattr(preference, 'notification')) + self.assertEqual(preference.notification, 'new_announcement') diff --git a/tests/test_course.py b/tests/test_course.py index 901508e4..cb9f46b4 100644 --- a/tests/test_course.py +++ b/tests/test_course.py @@ -10,12 +10,18 @@ from canvasapi.discussion_topic import DiscussionTopic from canvasapi.enrollment import Enrollment from canvasapi.exceptions import ResourceDoesNotExist, RequiredFieldMissing +from canvasapi.external_feed import ExternalFeed from canvasapi.external_tool import ExternalTool +from canvasapi.file import File +from canvasapi.folder import Folder from canvasapi.group import Group, GroupCategory from canvasapi.module import Module from canvasapi.quiz import Quiz from canvasapi.section import Section +from canvasapi.tab import Tab from canvasapi.user import User +from canvasapi.submission import Submission +from canvasapi.user import UserDisplay from tests import settings from tests.util import register_uris @@ -546,6 +552,232 @@ def test_create_external_tool(self, m): self.assertTrue(hasattr(response, 'id')) self.assertEqual(response.id, 20) + # get_course_level_participation_data() + def test_get_course_level_participation_data(self, m): + register_uris({'course': ['get_course_level_participation_data']}, m) + + response = self.course.get_course_level_participation_data() + + self.assertIsInstance(response, list) + + # get_course_level_assignment_data() + def test_get_course_level_assignment_data(self, m): + register_uris({'course': ['get_course_level_assignment_data']}, m) + + response = self.course.get_course_level_assignment_data() + + self.assertIsInstance(response, list) + + # get_course_level_student_summary_data() + def test_get_course_level_student_summary_data(self, m): + register_uris({'course': ['get_course_level_student_summary_data']}, m) + + response = self.course.get_course_level_student_summary_data() + + self.assertIsInstance(response, list) + + # get_user_in_a_course_level_participation_data() + def test_get_user_in_a_course_level_participation_data(self, m): + register_uris({'course': ['get_user_in_a_course_level_participation_data']}, m) + + response = self.course.get_user_in_a_course_level_participation_data(1) + + self.assertIsInstance(response, list) + + # get_user_in_a_course_level_assignment_data() + def test_get_user_in_a_course_level_assignment_data(self, m): + register_uris({'course': ['get_user_in_a_course_level_assignment_data']}, m) + + response = self.course.get_user_in_a_course_level_assignment_data(1) + + self.assertIsInstance(response, list) + + # get_user_in_a_course_level_messaging_data() + def test_get_user_in_a_course_level_messaging_data(self, m): + register_uris({'course': ['get_user_in_a_course_level_messaging_data']}, m) + + response = self.course.get_user_in_a_course_level_messaging_data(1) + + self.assertIsInstance(response, list) + + # submit_assignment() + def test_submit_assignment(self, m): + register_uris({'course': ['submit_assignment']}, m) + + assignment_id = 1 + sub_type = "online_upload" + sub_dict = {'submission_type': sub_type} + assignment = self.course.submit_assignment(assignment_id, sub_dict) + + self.assertIsInstance(assignment, Submission) + self.assertTrue(hasattr(assignment, 'submission_type')) + self.assertEqual(assignment.submission_type, sub_type) + + def test_subit_assignment_fail(self, m): + with self.assertRaises(RequiredFieldMissing): + self.course.submit_assignment(1, {}) + + # list_submissions() + def test_list_submissions(self, m): + register_uris({'course': ['list_submissions']}, m) + + assignment_id = 1 + submissions = self.course.list_submissions(assignment_id) + submission_list = [submission for submission in submissions] + + self.assertEqual(len(submission_list), 2) + self.assertIsInstance(submission_list[0], Submission) + + # list_multiple_submission() + def test_list_multiple_submissions(self, m): + register_uris({'course': ['list_multiple_submissions']}, m) + + submissions = self.course.list_multiple_submissions() + submission_list = [submission for submission in submissions] + + self.assertEqual(len(submission_list), 2) + self.assertIsInstance(submission_list[0], Submission) + + # get_submission() + def test_get_submission(self, m): + register_uris({'course': ['get_submission']}, m) + + assignment_id = 1 + user_id = 1 + submission = self.course.get_submission(assignment_id, user_id) + + self.assertIsInstance(submission, Submission) + self.assertTrue(hasattr(submission, 'submission_type')) + + # update_submission() + def test_update_submission(self, m): + register_uris({'course': ['update_submission', 'get_submission']}, m) + + assignment_id = 1 + user_id = 1 + submission = self.course.update_submission( + assignment_id, + user_id, + submission={'excuse': True} + ) + + self.assertIsInstance(submission, Submission) + self.assertTrue(hasattr(submission, 'excused')) + + # list_gradeable_students() + def test_list_gradeable_students(self, m): + register_uris({'course': ['list_gradeable_students']}, m) + + assignment_id = 1 + students = self.course.list_gradeable_students(assignment_id) + student_list = [student for student in students] + + self.assertEqual(len(student_list), 2) + self.assertIsInstance(student_list[0], UserDisplay) + + # mark_submission_as_read + def test_mark_submission_as_read(self, m): + register_uris({'course': ['mark_submission_as_read']}, m) + + submission_id = 1 + user_id = 1 + submission = self.course.mark_submission_as_read(submission_id, user_id) + + self.assertTrue(submission) + + # mark_submission_as_unread + def test_mark_submission_as_unread(self, m): + register_uris({'course': ['mark_submission_as_unread']}, m) + + submission_id = 1 + user_id = 1 + submission = self.course.mark_submission_as_unread(submission_id, user_id) + + self.assertTrue(submission) + + # list_external_feeds() + def test_list_external_feeds(self, m): + register_uris({'course': ['list_external_feeds']}, m) + + feeds = self.course.list_external_feeds() + feed_list = [feed for feed in feeds] + self.assertEqual(len(feed_list), 2) + self.assertTrue(hasattr(feed_list[0], 'url')) + self.assertIsInstance(feed_list[0], ExternalFeed) + + # create_external_feed() + def test_create_external_feed(self, m): + register_uris({'course': ['create_external_feed']}, m) + + url_str = "http://example.com/myblog.rss" + response = self.course.create_external_feed(url=url_str) + self.assertIsInstance(response, ExternalFeed) + + # delete_external_feed() + def test_delete_external_feed(self, m): + register_uris({'course': ['delete_external_feed']}, m) + + ef_id = 1 + deleted_ef = self.course.delete_external_feed(ef_id) + + self.assertIsInstance(deleted_ef, ExternalFeed) + self.assertTrue(hasattr(deleted_ef, 'url')) + self.assertEqual(deleted_ef.display_name, "My Blog") + + # list_files() + def test_course_files(self, m): + register_uris({'course': ['list_course_files', 'list_course_files2']}, m) + + files = self.course.list_files() + file_list = [file for file in files] + self.assertEqual(len(file_list), 4) + self.assertIsInstance(file_list[0], File) + + # get_folder() + def test_get_folder(self, m): + register_uris({'course': ['get_folder']}, m) + + folder = self.course.get_folder(1) + self.assertEqual(folder.name, "Folder 1") + self.assertIsInstance(folder, Folder) + + # list_folders() + def test_list_folders(self, m): + register_uris({'course': ['list_folders']}, m) + + folders = self.course.list_folders() + folder_list = [folder for folder in folders] + self.assertEqual(len(folder_list), 2) + self.assertIsInstance(folder_list[0], Folder) + + # create_folder() + def test_create_folder(self, m): + register_uris({'course': ['create_folder']}, m) + + name_str = "Test String" + response = self.course.create_folder(name=name_str) + self.assertIsInstance(response, Folder) + + # list_tabs() + def test_list_tabs(self, m): + register_uris({'course': ['list_tabs']}, m) + + tabs = self.course.list_tabs() + tab_list = [tab for tab in tabs] + self.assertEqual(len(tab_list), 2) + self.assertIsInstance(tab_list[0], Tab) + + # update_tab() + def test_update_tab(self, m): + register_uris({'course': ['update_tab']}, m) + + tab_id = "pages" + new_position = 3 + tab = self.course.update_tab(tab_id, position=new_position) + + self.assertIsInstance(tab, Tab) + self.assertEqual(tab.position, 3) + @requests_mock.Mocker() class TestCourseNickname(unittest.TestCase): diff --git a/tests/test_external_feed.py b/tests/test_external_feed.py new file mode 100644 index 00000000..e75cfa50 --- /dev/null +++ b/tests/test_external_feed.py @@ -0,0 +1,26 @@ +import unittest + +import requests_mock + +from canvasapi import Canvas +from tests import settings +from tests.util import register_uris + + +@requests_mock.Mocker() +class TestExternalFeed(unittest.TestCase): + + @classmethod + def setUp(self): + self.canvas = Canvas(settings.BASE_URL, settings.API_KEY) + + with requests_mock.Mocker() as m: + register_uris({'course': ['get_by_id', 'list_external_feeds']}, m) + + self.course = self.canvas.get_course(1) + self.external_feed = self.course.list_external_feeds()[0] + + # __str__() + def test__str__(self, m): + string = str(self.external_feed) + self.assertIsInstance(string, str) diff --git a/tests/test_file.py b/tests/test_file.py new file mode 100644 index 00000000..f9b406fc --- /dev/null +++ b/tests/test_file.py @@ -0,0 +1,37 @@ +import unittest + +import requests_mock + +from canvasapi import Canvas +from canvasapi.file import File +from tests import settings +from tests.util import register_uris + + +@requests_mock.Mocker() +class TestFile(unittest.TestCase): + + @classmethod + def setUp(self): + self.canvas = Canvas(settings.BASE_URL, settings.API_KEY) + + with requests_mock.Mocker() as m: + register_uris({'course': ['get_by_id', 'list_course_files', 'list_course_files2']}, m) + + self.course = self.canvas.get_course(1) + self.file = self.course.list_files()[0] + + # __str__() + def test__str__(self, m): + string = str(self.file) + self.assertIsInstance(string, str) + + # delete() + def test_delete_file(self, m): + register_uris({'file': ['delete_file']}, m) + + deleted_file = self.file.delete() + + self.assertIsInstance(deleted_file, File) + self.assertTrue(hasattr(deleted_file, 'display_name')) + self.assertEqual(deleted_file.display_name, "Bad File.docx") diff --git a/tests/test_folder.py b/tests/test_folder.py new file mode 100644 index 00000000..6eabd965 --- /dev/null +++ b/tests/test_folder.py @@ -0,0 +1,72 @@ +import unittest + +import requests_mock + +from canvasapi import Canvas +from canvasapi.file import File +from canvasapi.folder import Folder +from tests import settings +from tests.util import register_uris + + +@requests_mock.Mocker() +class TestFolder(unittest.TestCase): + + @classmethod + def setUp(self): + self.canvas = Canvas(settings.BASE_URL, settings.API_KEY) + + with requests_mock.Mocker() as m: + register_uris({'folder': ['get_by_id']}, m) + + self.folder = self.canvas.get_folder(1) + + # __str__() + def test__str__(self, m): + string = str(self.folder) + self.assertIsInstance(string, str) + + # list_files() + def test_folder_files(self, m): + register_uris({'folder': ['list_folder_files', 'list_folder_files2']}, m) + + files = self.folder.list_files() + file_list = [file for file in files] + self.assertEqual(len(file_list), 4) + self.assertIsInstance(file_list[0], File) + + # delete() + def test_delete_file(self, m): + register_uris({'folder': ['delete_folder']}, m) + + deleted_folder = self.folder.delete() + + self.assertIsInstance(deleted_folder, Folder) + self.assertTrue(hasattr(deleted_folder, 'name')) + self.assertEqual(deleted_folder.full_name, "course_files/Folder 1") + + # list_folders() + def test_list_folders(self, m): + register_uris({'folder': ['list_folders']}, m) + + folders = self.folder.list_folders() + folder_list = [folder for folder in folders] + self.assertEqual(len(folder_list), 2) + self.assertIsInstance(folder_list[0], Folder) + + # create_folder() + def test_create_folder(self, m): + register_uris({'folder': ['create_folder']}, m) + + name_str = "Test String" + response = self.folder.create_folder(name=name_str) + self.assertIsInstance(response, Folder) + + # update() + def test_update(self, m): + register_uris({'folder': ['update']}, m) + + new_name = 'New Name' + response = self.folder.update(name=new_name) + self.assertIsInstance(response, Folder) + self.assertEqual(self.folder.name, new_name) diff --git a/tests/test_group.py b/tests/test_group.py index ef117645..265bc3ce 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -9,6 +9,10 @@ from canvasapi.course import Page from canvasapi.discussion_topic import DiscussionTopic from canvasapi.exceptions import RequiredFieldMissing +from canvasapi.external_feed import ExternalFeed +from canvasapi.file import File +from canvasapi.folder import Folder +from canvasapi.tab import Tab from tests import settings from tests.util import register_uris @@ -255,6 +259,78 @@ def test_reorder_pinned_topics_no_list(self, m): with self.assertRaises(ValueError): self.group.reorder_pinned_topics(order=order) + # list_external_feeds() + def test_list_external_feeds(self, m): + register_uris({'group': ['list_external_feeds']}, m) + + feeds = self.group.list_external_feeds() + feed_list = [feed for feed in feeds] + self.assertEqual(len(feed_list), 2) + self.assertTrue(hasattr(feed_list[0], 'url')) + self.assertIsInstance(feed_list[0], ExternalFeed) + + # create_external_feed() + def test_create_external_feed(self, m): + register_uris({'group': ['create_external_feed']}, m) + + url_str = "http://example.com/myblog.rss" + response = self.group.create_external_feed(url=url_str) + self.assertIsInstance(response, ExternalFeed) + + # delete_external_feed() + def test_delete_external_feed(self, m): + register_uris({'group': ['delete_external_feed']}, m) + + ef_id = 1 + deleted_ef = self.group.delete_external_feed(ef_id) + + self.assertIsInstance(deleted_ef, ExternalFeed) + self.assertTrue(hasattr(deleted_ef, 'url')) + self.assertEqual(deleted_ef.display_name, "My Blog") + + # list_files() + def test_group_files(self, m): + register_uris({'group': ['list_group_files', 'list_group_files2']}, m) + + files = self.group.list_files() + file_list = [file for file in files] + self.assertEqual(len(file_list), 4) + self.assertIsInstance(file_list[0], File) + + # get_folder() + def test_get_folder(self, m): + register_uris({'group': ['get_folder']}, m) + + folder = self.group.get_folder(1) + self.assertEqual(folder.name, "Folder 1") + self.assertIsInstance(folder, Folder) + + # list_folders() + def test_list_folders(self, m): + register_uris({'group': ['list_folders']}, m) + + folders = self.group.list_folders() + folder_list = [folder for folder in folders] + self.assertEqual(len(folder_list), 2) + self.assertIsInstance(folder_list[0], Folder) + + # create_folder() + def test_create_folder(self, m): + register_uris({'group': ['create_folder']}, m) + + name_str = "Test String" + response = self.group.create_folder(name=name_str) + self.assertIsInstance(response, Folder) + + # list_tabs() + def test_list_tabs(self, m): + register_uris({'group': ['list_tabs']}, m) + + tabs = self.group.list_tabs() + tab_list = [tab for tab in tabs] + self.assertEqual(len(tab_list), 2) + self.assertIsInstance(tab_list[0], Tab) + @requests_mock.Mocker() class TestGroupMembership(unittest.TestCase): diff --git a/tests/test_login.py b/tests/test_login.py new file mode 100644 index 00000000..5e012234 --- /dev/null +++ b/tests/test_login.py @@ -0,0 +1,56 @@ +import unittest + +import requests_mock + +from canvasapi import Canvas +from canvasapi.login import Login +from tests import settings +from tests.util import register_uris + + +@requests_mock.Mocker() +class TestLogin(unittest.TestCase): + + @classmethod + def setUp(self): + self.canvas = Canvas(settings.BASE_URL, settings.API_KEY) + + with requests_mock.Mocker() as m: + register_uris({ + 'account': ['get_by_id', 'create_user_login'] + }, m) + + self.account = self.canvas.get_account(1) + self.login = self.account.create_user_login( + user={"id": 123}, + login={"unique_id": 112233} + ) + + # delete() + def test_delete_user_login(self, m): + register_uris({'login': ['delete_user_login']}, m) + + deleted_user_login = self.login.delete() + + self.assertIsInstance(deleted_user_login, Login) + self.assertTrue(hasattr(deleted_user_login, 'unique_id')) + self.assertEqual(deleted_user_login.unique_id, 112233) + + # edit() + def test_edit_user_login(self, m): + register_uris({'login': ['edit_user_login']}, m) + + unique_id = 112233 + edited_user_login = self.login.edit( + user={"id": 123}, + login={"unique_id": unique_id}, + ) + + self.assertIsInstance(edited_user_login, Login) + self.assertTrue(hasattr(edited_user_login, 'unique_id')) + self.assertEqual(edited_user_login.unique_id, unique_id) + + # __str__() + def test__str__(self, m): + string = str(self.login) + self.assertIsInstance(string, str) diff --git a/tests/test_notification_preference.py b/tests/test_notification_preference.py new file mode 100644 index 00000000..1a2fdcbb --- /dev/null +++ b/tests/test_notification_preference.py @@ -0,0 +1,31 @@ +import unittest + +import requests_mock + +from canvasapi import Canvas +from tests import settings +from tests.util import register_uris + + +@requests_mock.Mocker() +class TestNotificationPreference(unittest.TestCase): + + @classmethod + def setUp(self): + self.canvas = Canvas(settings.BASE_URL, settings.API_KEY) + + with requests_mock.Mocker() as m: + requires = { + 'user': ['get_by_id', 'list_comm_channels'], + 'communication_channel': ['get_preference'] + } + register_uris(requires, m) + + self.user = self.canvas.get_user(1) + self.comm_chan = self.user.list_communication_channels()[0] + self.notif_pref = self.comm_chan.get_preference('new_announcement') + + # __str__() + def test__str__(self, m): + string = str(self.notif_pref) + self.assertIsInstance(string, str) diff --git a/tests/test_section.py b/tests/test_section.py index a50dd933..e2459c84 100644 --- a/tests/test_section.py +++ b/tests/test_section.py @@ -4,7 +4,9 @@ from canvasapi import Canvas from canvasapi.enrollment import Enrollment +from canvasapi.exceptions import RequiredFieldMissing from canvasapi.section import Section +from canvasapi.submission import Submission from tests.util import register_uris @@ -62,3 +64,87 @@ def test_delete(self, m): deleted_section = self.section.delete() self.assertIsInstance(deleted_section, Section) + + # submit_assignment() + def test_submit_assignment(self, m): + register_uris({'section': ['submit_assignment']}, m) + + assignment_id = 1 + sub_type = "online_upload" + sub_dict = {'submission_type': sub_type} + assignment = self.section.submit_assignment(assignment_id, sub_dict) + + self.assertIsInstance(assignment, Submission) + self.assertTrue(hasattr(assignment, 'submission_type')) + self.assertEqual(assignment.submission_type, sub_type) + + def test_subit_assignment_fail(self, m): + with self.assertRaises(RequiredFieldMissing): + self.section.submit_assignment(1, {}) + + # list_submissions() + def test_list_submissions(self, m): + register_uris({'section': ['list_submissions']}, m) + + assignment_id = 1 + submissions = self.section.list_submissions(assignment_id) + submission_list = [submission for submission in submissions] + + self.assertEqual(len(submission_list), 2) + self.assertIsInstance(submission_list[0], Submission) + + # list_multiple_submission() + def test_list_multiple_submissions(self, m): + register_uris({'section': ['list_multiple_submissions']}, m) + + submissions = self.section.list_multiple_submissions() + submission_list = [submission for submission in submissions] + + self.assertEqual(len(submission_list), 2) + self.assertIsInstance(submission_list[0], Submission) + + # get_submission() + def test_get_submission(self, m): + register_uris({'section': ['get_submission']}, m) + + assignment_id = 1 + user_id = 1 + submission = self.section.get_submission(assignment_id, user_id) + + self.assertIsInstance(submission, Submission) + self.assertTrue(hasattr(submission, 'submission_type')) + + # update_submission() + def test_update_submission(self, m): + register_uris({'section': ['update_submission', 'get_submission']}, m) + + assignment_id = 1 + user_id = 1 + submission = self.section.update_submission( + assignment_id, + user_id, + submission={'excuse': True} + ) + + self.assertIsInstance(submission, Submission) + self.assertTrue(hasattr(submission, 'excused')) + + # mark_submission_as_read + def test_mark_submission_as_read(self, m): + register_uris({'section': ['mark_submission_as_read']}, m) + + submission_id = 1 + user_id = 1 + submission = self.section.mark_submission_as_read(submission_id, user_id) + + self.assertTrue(submission) + + # mark_submission_as_unread + def test_mark_submission_as_unread(self, m): + register_uris({'section': ['mark_submission_as_unread']}, m) + + submission_id = 1 + user_id = 1 + submission = self.section.mark_submission_as_unread(submission_id, user_id) + + self.assertTrue(submission) diff --git a/tests/test_submission.py b/tests/test_submission.py new file mode 100644 index 00000000..2c5bd5a2 --- /dev/null +++ b/tests/test_submission.py @@ -0,0 +1,28 @@ +import unittest + +import requests_mock + +from canvasapi import Canvas +from tests import settings +from tests.util import register_uris + + +@requests_mock.Mocker() +class TestSubmission(unittest.TestCase): + + @classmethod + def setUp(self): + self.canvas = Canvas(settings.BASE_URL, settings.API_KEY) + + with requests_mock.Mocker() as m: + register_uris({ + 'section': ['get_by_id', 'get_submission'] + }, m) + + self.section = self.canvas.get_section(1) + self.submission = self.section.get_submission(1, 1) + + # __str__() + def test__str__(self, m): + string = str(self.submission) + self.assertIsInstance(string, str) diff --git a/tests/test_tab.py b/tests/test_tab.py new file mode 100644 index 00000000..95683c6a --- /dev/null +++ b/tests/test_tab.py @@ -0,0 +1,32 @@ +import unittest + +import requests_mock + +from canvasapi import Canvas +from tests import settings +from tests.util import register_uris + + +@requests_mock.Mocker() +class TestTab(unittest.TestCase): + + @classmethod + def setUp(self): + self.canvas = Canvas(settings.BASE_URL, settings.API_KEY) + + with requests_mock.Mocker() as m: + register_uris({ + 'course': ['get_by_id', 'list_tabs'] + }, m) + + self.course = self.canvas.get_course(1) + + tabs = self.course.list_tabs() + tab_list = [tab for tab in tabs] + + self.tab = tab_list[0] + + # __str__() + def test__str__(self, m): + string = str(self.tab) + self.assertIsInstance(string, str) diff --git a/tests/test_user.py b/tests/test_user.py index 5847ed2d..68249b56 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -9,11 +9,15 @@ from canvasapi.avatar import Avatar from canvasapi.bookmark import Bookmark from canvasapi.calendar_event import CalendarEvent +from canvasapi.communication_channel import CommunicationChannel from canvasapi.course import Course +from canvasapi.file import File +from canvasapi.folder import Folder from canvasapi.group import Group from canvasapi.enrollment import Enrollment from canvasapi.page_view import PageView from canvasapi.user import User +from canvasapi.login import Login from tests import settings from tests.util import register_uris @@ -225,6 +229,15 @@ def test_list_calendar_events_for_user(self, m): self.assertEqual(len(cal_event_list), 2) self.assertIsInstance(cal_event_list[0], CalendarEvent) + # list_communication_channels() + def test_list_communication_channels(self, m): + register_uris({'user': ['list_comm_channels', 'list_comm_channels2']}, m) + + comm_channels = self.user.list_communication_channels() + channel_list = [channel for channel in comm_channels] + self.assertEqual(len(channel_list), 4) + self.assertIsInstance(channel_list[0], CommunicationChannel) + # list_bookmarks() def test_list_bookmarks(self, m): register_uris({'bookmark': ['list_bookmarks']}, m) @@ -254,3 +267,114 @@ def test_create_bookmark(self, m): self.assertIsInstance(evnt, Bookmark) self.assertEqual(evnt.name, "Test Bookmark") self.assertEqual(evnt.url, "https://www.google.com") + + # list_files() + def test_user_files(self, m): + register_uris({'user': ['get_user_files', 'get_user_files2']}, m) + + files = self.user.list_files() + file_list = [file for file in files] + self.assertEqual(len(file_list), 4) + self.assertIsInstance(file_list[0], File) + + # get_folder() + def test_get_folder(self, m): + register_uris({'user': ['get_folder']}, m) + + folder = self.user.get_folder(1) + self.assertEqual(folder.name, "Folder 1") + self.assertIsInstance(folder, Folder) + + # list_folders() + def test_list_folders(self, m): + register_uris({'user': ['list_folders']}, m) + + folders = self.user.list_folders() + folder_list = [folder for folder in folders] + self.assertEqual(len(folder_list), 2) + self.assertIsInstance(folder_list[0], Folder) + + # create_folder() + def test_create_folder(self, m): + register_uris({'user': ['create_folder']}, m) + + name_str = "Test String" + response = self.user.create_folder(name=name_str) + self.assertIsInstance(response, Folder) + + # list_user_logins() + def test_list_user_logins(self, m): + requires = {'user': ['list_user_logins', 'list_user_logins_2']} + register_uris(requires, m) + + response = self.user.list_user_logins() + login_list = [login for login in response] + + self.assertIsInstance(login_list[0], Login) + self.assertEqual(len(login_list), 2) + + # list_observees() + def test_list_observees(self, m): + requires = {'user': ['list_observees', 'list_observees_2']} + register_uris(requires, m) + + response = self.user.list_observees() + observees_list = [observees for observees in response] + + self.assertIsInstance(observees_list[0], User) + self.assertEqual(len(observees_list), 4) + + # add_observee_with_credentials() + def test_add_observee_with_credentials(self, m): + register_uris({'user': ['add_observee_with_credentials']}, m) + + response = self.user.add_observee_with_credentials() + + self.assertIsInstance(response, User) + + # show_observee() + def test_show_observee(self, m): + register_uris({'user': ['show_observee']}, m) + + response = self.user.show_observee(6) + + self.assertIsInstance(response, User) + + # add_observee() + def test_add_observee(self, m): + register_uris({'user': ['add_observee']}, m) + + response = self.user.add_observee(7) + + self.assertIsInstance(response, User) + + # remove_observee() + def test_remove_observee(self, m): + register_uris({'user': ['remove_observee']}, m) + + response = self.user.remove_observee(8) + + self.assertIsInstance(response, User) + + +@requests_mock.Mocker() +class TestUserDisplay(unittest.TestCase): + + @classmethod + def setUp(self): + self.canvas = Canvas(settings.BASE_URL, settings.API_KEY) + + with requests_mock.Mocker() as m: + register_uris({ + 'course': ['get_by_id', 'list_gradeable_students'] + }, m) + + self.course = self.canvas.get_course(1) + self.userDisplays = self.course.list_gradeable_students(1) + self.userDisplayList = [ud for ud in self.userDisplays] + self.userDisplay = self.userDisplayList[0] + + # __str__() + def test__str__(self, m): + string = str(self.userDisplay) + self.assertIsInstance(string, str)