diff --git a/.gitignore b/.gitignore index e829f1b4..3cfe0c52 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ \#*# .vscode/settings.json setup.sh +.python-version diff --git a/CHANGELOG.md b/CHANGELOG.md index ccfacf69..cf8ffb98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,44 @@ # Change Log +## [Unreleased] + +## [0.16.0] - 2020-06-26 + +### New Endpoint Coverage + +- Enrollment Terms + - Get a Single Enrollment Term (Thanks, [@lcamacho](https://github.com/lcamacho)) +- Files + - Resolve Path for Course (Thanks,[@dsavransky](https://github.com/dsavransky)) +- GraphQL (Thanks,[@jonespm](https://github.com/jonespm)) +- Late Policy (Thanks, [@kennygperez](https://github.com/kennygperez)) +- Quiz Assignment Overrides (Thanks, [@kennygperez](https://github.com/kennygperez)) +- Quiz Statistics (Thanks, [@andrew-gardener](https://github.com/andrew-gardener)) + +### General + +- Updated README to use updated parameters for getting a user's courses by enrollment state (Thanks,[@Vishvak365](https://github.com/Vishvak365)) + +### Deprecation Warnings + +- :warning: **_This is the final release with support for Python 2.7_** :warning: + - [Python 2.7 is end-of-life as of January 2020](https://www.python.org/doc/sunset-python-2/) + - Future releases of CanvasAPI will *NOT* support any version of Python 2 +- :warning: **_This is the final release with support for Python 3.4_** :warning: + - [Python 3.4 is end-of-life as of March 2019](https://www.python.org/downloads/release/python-3410/) + - Future releases of CanvasAPI will *NOT* support Python 3.4 or below +- This is the final deprecation warning for all methods marked as deprecated in this changelog or in our documentation. They will be removed in the next release. + +### Bugfixes + +- Fixed an issue where `Quiz.get_submission()` ignored data added from using the `include` kwarg. (Thanks,[@Mike-Nahmias](https://github.com/Mike-Nahmias)) +- Fixed the broken `__str__` method on the `ChangeRecord` class (Thanks,[@Mike-Nahmias](https://github.com/Mike-Nahmias)) +- Fixed an issue where printing an `AccountReport` would fail due to not having an ID (Thanks,[@Mike-Nahmias](https://github.com/Mike-Nahmias)) +- Fixed an issue where `"report_type"` was passed improperly (Thanks,[@brucespang](https://github.com/brucespang)) +- Fixed some new `flake8` issues (Thanks,[@dsavransky](https://github.com/dsavransky) and [@jonespm](https://github.com/jonespm)) +- Fixed an incorrect docstring for `Course.create_page()` (Thanks,[@dsavransky](https://github.com/dsavransky)) +- Fixed an issue where extra whitespace in the user-supplied canvas URL would break `PaginatedList` (Thanks,[@amorqiu](https://github.com/amorqiu)) + ## [0.15.0] - 2019-11-19 ### New Endpoint Coverage @@ -403,6 +442,8 @@ Huge thanks to [@liblit](https://github.com/liblit) for lots of issues, suggesti - Fixed some incorrectly defined parameters - Fixed an issue where tests would fail due to an improperly configured requires block +[Unreleased]: https://github.com/ucfopen/canvasapi/compare/v0.16.0...develop +[0.16.0]: https://github.com/ucfopen/canvasapi/compare/v0.15.0...v0.16.0 [0.15.0]: https://github.com/ucfopen/canvasapi/compare/v0.14.0...v0.15.0 [0.14.0]: https://github.com/ucfopen/canvasapi/compare/v0.13.0...v0.14.0 [0.13.0]: https://github.com/ucfopen/canvasapi/compare/v0.12.0...v0.13.0 diff --git a/canvasapi/__init__.py b/canvasapi/__init__.py index 5dd4b6f1..bc14ad6e 100644 --- a/canvasapi/__init__.py +++ b/canvasapi/__init__.py @@ -6,4 +6,4 @@ __all__ = ["Canvas"] -__version__ = "0.15.0" +__version__ = "0.16.0" diff --git a/canvasapi/account.py b/canvasapi/account.py index d03cea3d..f8817a2b 100644 --- a/canvasapi/account.py +++ b/canvasapi/account.py @@ -970,6 +970,27 @@ def get_enrollment(self, enrollment, **kwargs): ) return Enrollment(self._requester, response.json()) + def get_enrollment_term(self, term, **kwargs): + """ + Retrieve the details for an enrollment term in the account. Includes overrides by default. + + :calls: `GET /api/v1/accounts/:account_id/terms/:id \ + `_ + + :param term: The object or ID of the enrollment term to retrieve. + :type term: :class:`canvasapi.enrollment_term.EnrollmentTerm` or int + + :rtype: :class:`canvasapi.enrollment_term.EnrollmentTerm` + """ + from canvasapi.enrollment_term import EnrollmentTerm + + term_id = obj_or_id(term, "term", (EnrollmentTerm,)) + + response = self._requester.request( + "GET", "accounts/{}/terms/{}".format(self.id, term_id) + ) + return EnrollmentTerm(self._requester, response.json()) + def get_enrollment_terms(self, **kwargs): """ List enrollment terms for a context. @@ -1925,7 +1946,11 @@ def update_global_notification(self, account_notification, **kwargs): @python_2_unicode_compatible class AccountReport(CanvasObject): def __str__(self): - return "{} ({})".format(self.report, self.id) + try: + return "{} ({})".format(self.report, self.id) + except AttributeError: + # Print params if not a report instance + return "{} ({})".format(self.report, self.parameters) def delete_report(self, **kwargs): """ diff --git a/canvasapi/blueprint.py b/canvasapi/blueprint.py index ad40862e..f8e7fae9 100644 --- a/canvasapi/blueprint.py +++ b/canvasapi/blueprint.py @@ -236,8 +236,8 @@ def get_import_details(self, **kwargs): @python_2_unicode_compatible class ChangeRecord(CanvasObject): - def __str__(self): # pragma: no cover - return "{} {}".format(self.id, self.template_id) + def __str__(self): + return "{} {}".format(self.asset_id, self.asset_name) @python_2_unicode_compatible diff --git a/canvasapi/canvas.py b/canvasapi/canvas.py index 1ce81bda..3321e067 100644 --- a/canvasapi/canvas.py +++ b/canvasapi/canvas.py @@ -3,6 +3,7 @@ import warnings from canvasapi.account import Account +from canvasapi.comm_message import CommMessage from canvasapi.course import Course from canvasapi.course_epub_export import CourseEpubExport from canvasapi.current_user import CurrentUser @@ -14,7 +15,6 @@ from canvasapi.requester import Requester from canvasapi.section import Section from canvasapi.user import User -from canvasapi.comm_message import CommMessage from canvasapi.util import combine_kwargs, get_institution_url, obj_or_id @@ -59,12 +59,10 @@ def __init__(self, base_url, access_token): UserWarning, ) - # Ensure that the user-supplied access token contains no leading or - # trailing spaces that may cause issues when communicating with - # the API. + # Ensure that the user-supplied access token and base_url contain no leading or + # trailing spaces that might cause issues when communicating with the API. access_token = access_token.strip() - - base_url = new_url + "/api/v1/" + base_url = base_url.strip() self.__requester = Requester(base_url, access_token) @@ -1184,6 +1182,33 @@ def get_user_participants(self, appointment_group, **kwargs): _kwargs=combine_kwargs(**kwargs), ) + def graphql(self, query, variables=None, **kwargs): + """ + Makes a GraphQL formatted request to Canvas + + :calls: `POST /api/graphql \ + `_ + + :param query: The GraphQL query to execute as a String + :type query: str + :param variables: The variable values as required by the supplied query + :type variables: dict + + :rtype: dict + """ + response = self.__requester.request( + "POST", + "graphql", + headers={"Content-Type": "application/json"}, + _kwargs=combine_kwargs(**kwargs) + + [("query", query), ("variables", variables)], + # Needs to call special endpoint without api/v1 + _url=self.__requester.original_url + "/api/graphql", + json=True, + ) + + return response.json() + def list_appointment_groups(self, **kwargs): """ List appointment groups. diff --git a/canvasapi/course.py b/canvasapi/course.py index 08d723bc..2903a35a 100644 --- a/canvasapi/course.py +++ b/canvasapi/course.py @@ -358,6 +358,26 @@ def create_group_category(self, name, **kwargs): ) return GroupCategory(self._requester, response.json()) + def create_late_policy(self, **kwargs): + """ + Create a late policy. If the course already has a late policy, a bad_request + is returned since there can only be one late policy per course. + + :calls: `POST /api/v1/courses/:id/late_policy \ + `_ + + :rtype: :class:`canvasapi.course.LatePolicy` + """ + + response = self._requester.request( + "POST", + "courses/{}/late_policy".format(self.id), + _kwargs=combine_kwargs(**kwargs), + ) + late_policy_json = response.json() + + return LatePolicy(self._requester, late_policy_json["late_policy"]) + def create_module(self, module, **kwargs): """ Create a new module. @@ -394,10 +414,10 @@ def create_page(self, wiki_page, **kwargs): :calls: `POST /api/v1/courses/:course_id/pages \ `_ - :param title: The title for the page. - :type title: dict + :param wiki_page: The title for the page. + :type wiki_page: dict :returns: The created page. - :rtype: :class:`canvasapi.course.Course` + :rtype: :class:`canvasapi.page.Page` """ if isinstance(wiki_page, dict) and "title" in wiki_page: @@ -554,6 +574,25 @@ def edit_front_page(self, **kwargs): return Page(self._requester, page_json) + def edit_late_policy(self, **kwargs): + """ + Patch a late policy. No body is returned upon success. + + :calls: `PATCH /api/v1/courses/:id/late_policy \ + `_ + + :returns: True if Late Policy was updated successfully. False otherwise. + :rtype: bool + """ + + response = self._requester.request( + "PATCH", + "courses/{}/late_policy".format(self.id), + _kwargs=combine_kwargs(**kwargs), + ) + + return response.status_code == 204 + def enroll_user(self, user, enrollment_type, **kwargs): """ Create a new user enrollment for a course or a section. @@ -1410,6 +1449,25 @@ def get_groups(self, **kwargs): _kwargs=combine_kwargs(**kwargs), ) + def get_late_policy(self, **kwargs): + """ + Returns the late policy for a course. + + :calls: `GET /api/v1/courses/:id/late_policy \ + `_ + + :rtype: :class:`canvasapi.course.LatePolicy` + """ + + response = self._requester.request( + "GET", + "courses/{}/late_policy".format(self.id), + _kwargs=combine_kwargs(**kwargs), + ) + late_policy_json = response.json() + + return LatePolicy(self._requester, late_policy_json["late_policy"]) + def get_licenses(self, **kwargs): """ Returns a paginated list of the licenses that can be applied to the @@ -1698,6 +1756,29 @@ def get_quiz(self, quiz): return Quiz(self._requester, quiz_json) + def get_quiz_overrides(self, **kwargs): + """ + Retrieve the actual due-at, unlock-at, + and available-at dates for quizzes based on + the assignment overrides active for the current API user. + + :calls: `GET /api/v1/courses/:course_id/quizzes/assignment_overrides \ + `_ + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.quiz.QuizAssignmentOverrideSet` + """ + from canvasapi.quiz import QuizAssignmentOverrideSet + + return PaginatedList( + QuizAssignmentOverrideSet, + self._requester, + "GET", + "courses/{}/quizzes/assignment_overrides".format(self.id), + _root="quiz_assignment_overrides", + _kwargs=combine_kwargs(**kwargs), + ) + def get_quizzes(self, **kwargs): """ Return a list of quizzes belonging to this course. @@ -2577,6 +2658,28 @@ def reset(self): ) return Course(self._requester, response.json()) + def resolve_path(self, full_path, **kwargs): + """ + Returns the paginated list of all of the folders in the given + path starting at the course root folder. + + :calls: `GET /api/v1/courses/:course_id/folders/by_path/*full_path \ + `_ + + :param full_path: Full path to resolve, relative to course root + :type full_path: string + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.folder.Folder` + """ + return PaginatedList( + Folder, + self._requester, + "GET", + "courses/{0}/folders/by_path/{1}".format(self.id, full_path), + _kwargs=combine_kwargs(**kwargs), + ) + def set_quiz_extensions(self, quiz_extensions, **kwargs): """ Set extensions for student all quiz submissions in a course. @@ -2725,8 +2828,8 @@ def update(self, **kwargs): :calls: `PUT /api/v1/courses/:id \ `_ - :returns: True if the course was updated, False otherwise. - :rtype: bool + :returns: `True` if the course was updated, `False` otherwise. + :rtype: `bool` """ response = self._requester.request( "PUT", "courses/{}".format(self.id), _kwargs=combine_kwargs(**kwargs) @@ -2878,3 +2981,9 @@ def remove(self): "DELETE", "users/self/course_nicknames/{}".format(self.course_id) ) return CourseNickname(self._requester, response.json()) + + +@python_2_unicode_compatible +class LatePolicy(CanvasObject): + def __str__(self): + return "Late Policy {}".format(self.id) diff --git a/canvasapi/exceptions.py b/canvasapi/exceptions.py index e68f232a..e118bc57 100644 --- a/canvasapi/exceptions.py +++ b/canvasapi/exceptions.py @@ -65,3 +65,9 @@ class Conflict(CanvasException): """Canvas had a conflict with an existing resource.""" pass + + +class UnprocessableEntity(CanvasException): + """Canvas was unable to process the entity.""" + + pass diff --git a/canvasapi/quiz.py b/canvasapi/quiz.py index 4e149c06..1c3072f1 100644 --- a/canvasapi/quiz.py +++ b/canvasapi/quiz.py @@ -8,6 +8,8 @@ from canvasapi.exceptions import RequiredFieldMissing from canvasapi.paginated_list import PaginatedList from canvasapi.quiz_group import QuizGroup +from canvasapi.submission import Submission +from canvasapi.user import User from canvasapi.util import combine_kwargs, obj_or_id @@ -141,7 +143,7 @@ def create_report(self, report_type, **kwargs): "Param `report_type` must be a either 'student_analysis' or 'item_analysis'" ) - kwargs["report_type"] = report_type + kwargs["quiz_report"] = {"report_type": report_type} response = self._requester.request( "POST", @@ -374,9 +376,45 @@ def get_quiz_submission(self, quiz_submission, **kwargs): response_json = response.json()["quiz_submissions"][0] response_json.update({"course_id": self.course_id}) + if len(response.json().get("quizzes", [])) > 0: + response_json.update( + {"quiz": Quiz(self._requester, response.json()["quizzes"][0])} + ) + if len(response.json().get("submissions", [])) > 0: + response_json.update( + { + "submission": Submission( + self._requester, response.json()["submissions"][0] + ) + } + ) + if len(response.json().get("users", [])) > 0: + response_json.update( + {"user": User(self._requester, response.json()["users"][0])} + ) return QuizSubmission(self._requester, response_json) + def get_statistics(self, **kwargs): + """ + Get statistics for for all quiz versions, or the latest quiz version. + + :calls: `GET /api/v1/courses/:course_id/quizzes/:quiz_id/statistics \ + `_ + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.quiz.QuizStatistic` + """ + return PaginatedList( + QuizStatistic, + self._requester, + "GET", + "courses/{}/quizzes/{}/statistics".format(self.course_id, self.id), + {"course_id": self.course_id}, + _root="quiz_statistics", + _kwargs=combine_kwargs(**kwargs), + ) + def get_submissions(self, **kwargs): """ Get a list of all submissions for this quiz. @@ -452,6 +490,12 @@ def set_extensions(self, quiz_extensions, **kwargs): ] +@python_2_unicode_compatible +class QuizStatistic(CanvasObject): + def __str__(self): + return "Quiz Statistic {}".format(self.id) + + @python_2_unicode_compatible class QuizSubmission(CanvasObject): def __str__(self): @@ -842,3 +886,9 @@ def unflag(self, validation_token=None, **kwargs): super(QuizSubmissionQuestion, self).set_attributes(question) return True + + +@python_2_unicode_compatible +class QuizAssignmentOverrideSet(CanvasObject): + def __str__(self): + return "Overrides for quiz_id {}".format(self.quiz_id) diff --git a/canvasapi/requester.py b/canvasapi/requester.py index 36ad89c9..fbb3cf9e 100644 --- a/canvasapi/requester.py +++ b/canvasapi/requester.py @@ -13,6 +13,7 @@ InvalidAccessToken, ResourceDoesNotExist, Unauthorized, + UnprocessableEntity, ) from canvasapi.util import clean_headers @@ -32,12 +33,14 @@ def __init__(self, base_url, access_token): :param access_token: The API key to authenticate requests with. :type access_token: str """ - self.base_url = base_url + # Preserve the original base url and add "/api/v1" to it + self.original_url = base_url + self.base_url = base_url + "/api/v1/" self.access_token = access_token self._session = requests.Session() self._cache = [] - def _delete_request(self, url, headers, data=None): + def _delete_request(self, url, headers, data=None, **kwargs): """ Issue a DELETE request to the specified endpoint with the data provided. @@ -50,7 +53,7 @@ def _delete_request(self, url, headers, data=None): """ return self._session.delete(url, headers=headers, data=data) - def _get_request(self, url, headers, params=None): + def _get_request(self, url, headers, params=None, **kwargs): """ Issue a GET request to the specified endpoint with the data provided. @@ -63,7 +66,7 @@ def _get_request(self, url, headers, params=None): """ return self._session.get(url, headers=headers, params=params) - def _patch_request(self, url, headers, data=None): + def _patch_request(self, url, headers, data=None, **kwargs): """ Issue a PATCH request to the specified endpoint with the data provided. @@ -76,7 +79,7 @@ def _patch_request(self, url, headers, data=None): """ return self._session.patch(url, headers=headers, data=data) - def _post_request(self, url, headers, data=None): + def _post_request(self, url, headers, data=None, json=False): """ Issue a POST request to the specified endpoint with the data provided. @@ -86,7 +89,11 @@ def _post_request(self, url, headers, data=None): :type headers: dict :param data: The data to send with this request. :type data: dict + :param json: Whether or not to send the data as json + :type json: bool """ + if json: + return self._session.post(url, headers=headers, json=dict(data)) # Grab file from data. files = None @@ -103,7 +110,7 @@ def _post_request(self, url, headers, data=None): return self._session.post(url, headers=headers, data=data, files=files) - def _put_request(self, url, headers, data=None): + def _put_request(self, url, headers, data=None, **kwargs): """ Issue a PUT request to the specified endpoint with the data provided. @@ -124,6 +131,7 @@ def request( use_auth=True, _url=None, _kwargs=None, + json=False, **kwargs ): """ @@ -146,6 +154,10 @@ def request( :param _kwargs: A list of 2-tuples representing processed keyword arguments to be sent to Canvas as params or data. :type _kwargs: `list` + :param json: Whether or not to treat the data as json instead of form data. + currently only the POST request of GraphQL is using this parameter. + For all other methods it's just passed and ignored. + :type json: `bool` :rtype: str """ full_url = _url if _url else "{}{}".format(self.base_url, endpoint) @@ -194,7 +206,7 @@ def request( if _kwargs: logger.debug("Data: {data}".format(data=pformat(_kwargs))) - response = req_method(full_url, headers, _kwargs) + response = req_method(full_url, headers, _kwargs, json=json) logger.info( "Response: {method} {url} {status}".format( method=method, url=full_url, status=response.status_code @@ -231,6 +243,8 @@ def request( raise ResourceDoesNotExist("Not Found") elif response.status_code == 409: raise Conflict(response.text) + elif response.status_code == 422: + raise UnprocessableEntity(response.text) elif response.status_code > 400: # generic catch-all for error codes raise CanvasException( diff --git a/docs/course-ref.rst b/docs/course-ref.rst index f2df1d3d..3d43f672 100644 --- a/docs/course-ref.rst +++ b/docs/course-ref.rst @@ -10,4 +10,11 @@ CourseNickname ============== .. autoclass:: canvasapi.course.CourseNickname - :members: + :members: + +========== +LatePolicy +========== + +.. autoclass:: canvasapi.course.LatePolicy + :members: diff --git a/docs/exceptions.rst b/docs/exceptions.rst index c8ffec5a..cc3419b2 100644 --- a/docs/exceptions.rst +++ b/docs/exceptions.rst @@ -21,6 +21,8 @@ Quick Guide +-----------------------------------------------------+-----------------+---------------------------------------------------------------------------------+ | :class:`~canvasapi.exceptions.Conflict` | 409 | Canvas had a conflict with an existing resource. | +-----------------------------------------------------+-----------------+---------------------------------------------------------------------------------+ +| :class:`~canvasapi.exceptions.UnprocessableEntity` | 422 | Canvas was unable to process the request. | ++-----------------------------------------------------+-----------------+---------------------------------------------------------------------------------+ | :class:`~canvasapi.exceptions.RequiredFieldMissing` | N/A | A required keyword argument was not included. | +-----------------------------------------------------+-----------------+---------------------------------------------------------------------------------+ | :class:`~canvasapi.exceptions.CanvasException` | N/A | An unknown error was thrown. | diff --git a/docs/quiz-ref.rst b/docs/quiz-ref.rst index 74a24a47..32b779da 100644 --- a/docs/quiz-ref.rst +++ b/docs/quiz-ref.rst @@ -5,6 +5,13 @@ Quiz .. autoclass:: canvasapi.quiz.Quiz :members: +========================= +QuizAssignmentOverrideSet +========================= + +.. autoclass:: canvasapi.quiz.QuizAssignmentOverrideSet + :members: + ============= QuizExtension ============= @@ -32,3 +39,17 @@ QuizSubmission .. autoclass:: canvasapi.quiz.QuizSubmission :members: + +=================== +QuizSubmissionEvent +=================== + +.. autoclass:: canvasapi.quiz.QuizSubmissionEvent + :members: + +====================== +QuizSubmissionQuestion +====================== + +.. autoclass:: canvasapi.quiz.QuizSubmissionQuestion + :members: diff --git a/tests/fixtures/account.json b/tests/fixtures/account.json index af424bce..819ac408 100644 --- a/tests/fixtures/account.json +++ b/tests/fixtures/account.json @@ -315,6 +315,15 @@ ], "status_code": 200 }, + "get_enrollment_term": { + "method": "GET", + "endpoint": "accounts/1/terms/1", + "data": { + "id": 1, + "name": "Enrollment Term 1" + }, + "status_code": 200 + }, "get_enrollment_terms": { "method": "GET", "endpoint": "accounts/1/terms", @@ -578,10 +587,12 @@ "endpoint": "accounts/1/reports", "data": [ { - "id": 1 + "id": 1, + "report": "sis_export_csv" }, { - "id": 2 + "id": 2, + "report": "grade_export_csv" } ], "headers": { diff --git a/tests/fixtures/course.json b/tests/fixtures/course.json index 0fab725e..45b03aa5 100644 --- a/tests/fixtures/course.json +++ b/tests/fixtures/course.json @@ -409,6 +409,50 @@ }, "status_code": 200 }, + "get_quiz_overrides": { + "method": "GET", + "endpoint": "courses/1/quizzes/assignment_overrides", + "data": { + "quiz_assignment_overrides": [{ + "quiz_id": "1", + "due_dates": [{ + "id": 1, + "due_at": "2014-02-21T06:59:59Z", + "unlock_at": null, + "lock_at": "2014-02-21T06:59:59Z", + "title": "Project X", + "base": true + }], + "all_dates": [{ + "id": 1, + "due_at": "2014-02-21T06:59:59Z", + "unlock_at": null, + "lock_at": "2014-02-21T06:59:59Z", + "title": "Project X", + "base": true + }] + },{ + "quiz_id": "2", + "due_dates": [{ + "id": 1, + "due_at": "2014-02-21T06:59:59Z", + "unlock_at": null, + "lock_at": "2014-02-21T06:59:59Z", + "title": "Project X", + "base": true + }], + "all_dates": [{ + "id": 1, + "due_at": "2014-02-21T06:59:59Z", + "unlock_at": null, + "lock_at": "2014-02-21T06:59:59Z", + "title": "Project X", + "base": true + }] + }] + }, + "status_code": 200 + }, "get_recent_students": { "method": "GET", "endpoint": "courses/1/recent_students", @@ -2044,6 +2088,51 @@ }, "status_code": 200 }, + "get_late_policy": { + "method": "GET", + "endpoint": "courses/1/late_policy", + "data": { + "late_policy": { + "id": 123, + "course_id": 123, + "missing_submission_deduction_enabled": true, + "missing_submission_deduction": 12.34, + "late_submission_deduction_enabled": true, + "late_submission_deduction": 12.34, + "late_submission_interval": "hour", + "late_submission_minimum_percent_enabled": true, + "late_submission_minimum_percent": 12.34, + "created_at": "2012-07-01T23:59:00-06:00", + "updated_at": "2012-07-01T23:59:00-06:00" + } + }, + "status_code": 200 + }, + "edit_late_policy": { + "method": "PATCH", + "endpoint": "courses/1/late_policy", + "status_code": 204 + }, + "create_late_policy": { + "method": "POST", + "endpoint": "courses/1/late_policy", + "data": { + "late_policy": { + "id": 123, + "course_id": 123, + "missing_submission_deduction_enabled": true, + "missing_submission_deduction": 12.34, + "late_submission_deduction_enabled": true, + "late_submission_deduction": 12.34, + "late_submission_interval": "hour", + "late_submission_minimum_percent_enabled": true, + "late_submission_minimum_percent": 12.34, + "created_at": "2012-07-01T23:59:00-06:00", + "updated_at": "2012-07-01T23:59:00-06:00" + } + }, + "status_code": 200 + }, "get_licenses": { "method": "GET", "endpoint": "courses/1/content_licenses", @@ -2060,5 +2149,41 @@ } ], "status_code": 200 + }, + "resolve_path": { + "method": "GET", + "endpoint": "courses/1/folders/by_path/Folder_Level_1/Folder_Level_2/Folder_Level_3", + "data": [ + { + "id": 2, + "files_count": 0, + "folders_count": 1, + "name": "course_files", + "full_name": "course_files" + }, + + { + "id": 3, + "files_count": 0, + "folders_count": 1, + "name": "Folder_Level_1", + "full_name": "course_files" + }, + { + "id": 4, + "files_count": 0, + "folders_count": 1, + "name": "Folder_Level_2", + "full_name": "course_files/Folder_Level_1/Folder_Level_2" + }, + { + "id": 5, + "files_count": 0, + "folders_count": 0, + "name": "Folder_Level_3", + "full_name": "course_files/Folder_Level_1/Folder_Level_2/Folder_Level_3" + } + ], + "status_code": 200 } } diff --git a/tests/fixtures/graphql.json b/tests/fixtures/graphql.json new file mode 100644 index 00000000..5f19eaf1 --- /dev/null +++ b/tests/fixtures/graphql.json @@ -0,0 +1,21 @@ +{ + "graphql": { + "method": "POST", + "endpoint": "graphql", + "data": { + "term": { + "coursesConnection": { + "nodes": [ + { + "_id": "1", + "assignmentsConnection": { + "nodes": [] + } + } + ] + } + } + }, + "status_code": 200 + } +} \ No newline at end of file diff --git a/tests/fixtures/quiz.json b/tests/fixtures/quiz.json index 22e753a2..253e05cd 100644 --- a/tests/fixtures/quiz.json +++ b/tests/fixtures/quiz.json @@ -219,6 +219,30 @@ "validation_token": "this is a validation token", "score": 0 } + ], + "quizzes": [ + { + "id": 1, + "title": "Test Quiz", + "quiz_type": "survey" + } + ], + "submissions": [ + { + "id": 1, + "body": "user: 1, quiz: 1, score: 1.0, time: 2020-01-01 00:00:00Z", + "grade": "1", + "score": 1, + "assignment_id": 1234, + "user_id": 1 + } + ], + "users": [ + { + "id": 1, + "name": "Test User", + "login_id": "testuser@example.com" + } ] }, "status_code": 200 @@ -303,6 +327,234 @@ }, "status_code": 200 }, + "get_statistics": { + "method": "GET", + "endpoint": "courses/1/quizzes/1/statistics", + "data": { + "quiz_statistics": [ + { + "id": "1", + "url": "http://localhost:3000/api/v1/courses/12/quizzes/43/statistics", + "html_url": "http://localhost:3000/courses/12/quizzes/43/statistics", + "multiple_attempts_exist": false, + "generated_at": "2019-11-20T18:48:20Z", + "includes_all_versions": false, + "includes_sis_ids": true, + "points_possible": null, + "anonymous_survey": false, + "speed_grader_url": null, + "quiz_submissions_zip_url": "http://localhost:3000/courses/12/quizzes/43/submissions?zip=1", + "question_statistics": [ + { + "id": "47", + "question_type": "multiple_dropdowns_question", + "question_text": "\n

What is your year of study? [year]

\n

What is your program? [program]

\n ", + "position": 1, + "responses": 3, + "answered": 3, + "correct": 0, + "partially_correct": 2, + "incorrect": 1, + "answer_sets": [ + { + "id": "84cdc76cabf41bd7c961f6ab12f117d8", + "text": "year", + "answers": [ + { + "id": "6100", + "text": "1", + "correct": true, + "responses": 0, + "user_names": [] + }, + { + "id": "8629", + "text": "2", + "correct": false, + "responses": 1, + "user_names": [ + "Student12 Student12" + ] + }, + { + "id": "6110", + "text": "3", + "correct": false, + "responses": 2, + "user_names": [ + "Student11 Student11", + "Student10 Student10" + ] + }, + { + "id": "9262", + "text": "4", + "correct": false, + "responses": 0, + "user_names": [] + }, + { + "id": "279", + "text": "5", + "correct": false, + "responses": 0, + "user_names": [] + }, + { + "id": "5721", + "text": "6+", + "correct": false, + "responses": 0, + "user_names": [] + } + ] + }, + { + "id": "a9c449d4fa44e9e5a41c574ae55ce4d9", + "text": "program", + "answers": [ + { + "id": "7270", + "text": "CompSci", + "correct": true, + "responses": 2, + "user_names": [ + "Student11 Student11", + "Student10 Student10" + ] + }, + { + "id": "2688", + "text": "Other", + "correct": false, + "responses": 1, + "user_names": [ + "Student12 Student12" + ] + } + ] + } + ] + }, + { + "id": "48", + "question_type": "multiple_choice_question", + "question_text": "How good are you at programming?", + "position": 2, + "responses": 3, + "answers": [ + { + "id": "1833", + "text": "Great", + "correct": true, + "responses": 0, + "user_names": [] + }, + { + "id": "7532", + "text": "Good", + "correct": false, + "responses": 2, + "user_names": [ + "Student12 Student12", + "Student10 Student10" + ] + }, + { + "id": "136", + "text": "Mediocre", + "correct": false, + "responses": 0, + "user_names": [] + }, + { + "id": "9694", + "text": "Bad", + "correct": false, + "responses": 1, + "user_names": [ + "Student11 Student11" + ] + }, + { + "id": "6452", + "text": "Terrible", + "correct": false, + "responses": 0, + "user_names": [] + } + ], + "answered_student_count": 3, + "top_student_count": 2, + "middle_student_count": 0, + "bottom_student_count": 1, + "correct_student_count": 0, + "incorrect_student_count": 3, + "correct_student_ratio": 0.0, + "incorrect_student_ratio": 1.0, + "correct_top_student_count": 0, + "correct_middle_student_count": 0, + "correct_bottom_student_count": 0, + "variance": 0.0, + "stdev": 0.0, + "difficulty_index": 0.0, + "alpha": null, + "point_biserials": [ + { + "answer_id": 1833, + "point_biserial": null, + "correct": true, + "distractor": false + }, + { + "answer_id": 7532, + "point_biserial": -0.49999999999999994, + "correct": false, + "distractor": true + }, + { + "answer_id": 136, + "point_biserial": null, + "correct": false, + "distractor": true + }, + { + "answer_id": 9694, + "point_biserial": 0.49999999999999994, + "correct": false, + "distractor": true + }, + { + "answer_id": 6452, + "point_biserial": null, + "correct": false, + "distractor": true + } + ] + } + ], + "submission_statistics": { + "scores": { + "0": 1, + "25": 2 + }, + "score_average": 0.3333333333333333, + "score_high": 0.5, + "score_low": 0.0, + "score_stdev": 0.2357022603955158, + "correct_count_average": 0.0, + "incorrect_count_average": 1.333333333333333, + "duration_average": 6.0, + "unique_count": 3 + }, + "links": { + "quiz": "http://localhost:3000/api/v1/courses/12/quizzes/43" + } + } + ] + }, + "status_code": 200 + }, "create_report": { "method": "POST", "endpoint": "courses/1/quizzes/1/reports", diff --git a/tests/fixtures/requests.json b/tests/fixtures/requests.json index 5d00c691..ca123fca 100644 --- a/tests/fixtures/requests.json +++ b/tests/fixtures/requests.json @@ -32,6 +32,12 @@ "data": {}, "status_code": 409 }, + "422": { + "method": "ANY", + "endpoint": "422", + "data": {}, + "status_code": 422 + }, "500": { "method": "ANY", "endpoint": "500", diff --git a/tests/settings.py b/tests/settings.py index 4fbbcab8..679cc118 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,10 +1,12 @@ from __future__ import absolute_import, division, print_function, unicode_literals BASE_URL = "https://example.com" +BASE_URL_GRAPHQL = "https://example.com/api/" BASE_URL_WITH_VERSION = "https://example.com/api/v1/" BASE_URL_AS_HTTP = "http://example.com" BASE_URL_AS_BLANK = "" BASE_URL_AS_INVALID = "example.com" +BASE_URL_WITH_EXTRA_SPACES = " https://example.com " API_KEY = "123" INVALID_ID = 9001 diff --git a/tests/test_account.py b/tests/test_account.py index e79d275e..d3b36579 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -286,6 +286,7 @@ def test_get_report(self, m): self.assertIsInstance(report, AccountReport) self.assertTrue(hasattr(report, "title")) self.assertEqual(report.title, "Zero Activity") + self.assertIsInstance(str(report), str) # get_index_of_reports() def test_get_index_of_reports(self, m): @@ -310,6 +311,7 @@ def test_get_reports(self, m): self.assertEqual(len(reports_list), 4) self.assertIsInstance(reports_list[0], AccountReport) self.assertTrue(hasattr(reports_list[0], "id")) + self.assertIsInstance(str(reports_list[0]), str) # get_subaccounts() def test_get_subaccounts(self, m): @@ -574,6 +576,19 @@ def test_list_enrollment_terms(self, m): self.assertEqual(len(warning_list), 1) self.assertEqual(warning_list[-1].category, DeprecationWarning) + # get_enrollment_term() + def test_get_enrollment_term(self, m): + register_uris({"account": ["get_enrollment_term"]}, m) + + enrollment_term = self.account.get_enrollment_term(1) + + self.assertIsInstance(enrollment_term, EnrollmentTerm) + + self.assertTrue(hasattr(enrollment_term, "id")) + self.assertEqual(enrollment_term.id, 1) + self.assertTrue(hasattr(enrollment_term, "name")) + self.assertEqual(enrollment_term.name, "Enrollment Term 1") + # get_enrollment_terms() def test_get_enrollment_terms(self, m): register_uris({"account": ["get_enrollment_terms"]}, m) diff --git a/tests/test_blueprint.py b/tests/test_blueprint.py index fd4b60a2..6b3c3b75 100644 --- a/tests/test_blueprint.py +++ b/tests/test_blueprint.py @@ -196,3 +196,25 @@ def test_get_import_details(self, m): import_details = self.b_import.get_import_details() self.assertIsInstance(import_details, PaginatedList) self.assertIsInstance(import_details[0], ChangeRecord) + + +@requests_mock.Mocker() +class TestChangeRecord(unittest.TestCase): + def setUp(self): + self.canvas = Canvas(settings.BASE_URL, settings.API_KEY) + + with requests_mock.Mocker() as m: + requires = { + "course": ["get_blueprint", "get_by_id"], + "blueprint": ["get_unsynced_changes"], + } + register_uris(requires, m) + + self.course = self.canvas.get_course(1) + self.blueprint = self.course.get_blueprint(1) + self.change_record = self.blueprint.get_unsynced_changes()[0] + + # __str__() + def test__str__(self, m): + string = str(self.change_record) + self.assertIsInstance(string, str) diff --git a/tests/test_canvas.py b/tests/test_canvas.py index 7a5a78d9..4256dce2 100644 --- a/tests/test_canvas.py +++ b/tests/test_canvas.py @@ -12,20 +12,19 @@ from canvasapi.account import Account from canvasapi.appointment_group import AppointmentGroup from canvasapi.calendar_event import CalendarEvent +from canvasapi.comm_message import CommMessage from canvasapi.conversation import Conversation from canvasapi.course import Course, CourseNickname from canvasapi.course_epub_export import CourseEpubExport from canvasapi.discussion_topic import DiscussionTopic -from canvasapi.exceptions import RequiredFieldMissing +from canvasapi.exceptions import RequiredFieldMissing, ResourceDoesNotExist from canvasapi.file import File from canvasapi.group import Group, GroupCategory -from canvasapi.exceptions import ResourceDoesNotExist from canvasapi.outcome import Outcome, OutcomeGroup from canvasapi.paginated_list import PaginatedList from canvasapi.progress import Progress from canvasapi.section import Section from canvasapi.user import User -from canvasapi.comm_message import CommMessage from tests import settings from tests.util import register_uris @@ -75,6 +74,12 @@ def test_init_strips_extra_spaces_in_api_key(self, m): client = Canvas(settings.BASE_URL, " 12345 ") self.assertEqual(client._Canvas__requester.access_token, "12345") + def test_init_strips_extra_spaces_in_base_url(self, m): + client = Canvas(settings.BASE_URL_WITH_EXTRA_SPACES, "12345") + self.assertEqual( + client._Canvas__requester.base_url, settings.BASE_URL_WITH_VERSION + ) + # create_account() def test_create_account(self, m): register_uris({"account": ["create"]}, m) @@ -832,3 +837,34 @@ def test_get_comm_messages(self, m): self.assertEqual(comm_messages[0].subject, "example subject line") self.assertEqual(comm_messages[1].id, 2) self.assertEqual(comm_messages[1].subject, "My Subject") + + def test_graphql(self, m): + register_uris({"graphql": ["graphql"]}, m, base_url=settings.BASE_URL_GRAPHQL) + query = """ + query MyQuery($termid: ID!) { + term(id: $termid) { + coursesConnection { + nodes { + _id + assignmentsConnection { + nodes { + name + _id + expectsExternalSubmission + } + } + } + edges { + node { + id + } + } + } + } + } + """ + variables = {"termid": 125} + + graphql_response = self.canvas.graphql(query=query, variables=variables) + # Just a super simple check right now that it gets back a dict respose + self.assertIsInstance(graphql_response, dict) diff --git a/tests/test_course.py b/tests/test_course.py index b07c9ba6..43cd36bb 100644 --- a/tests/test_course.py +++ b/tests/test_course.py @@ -12,7 +12,7 @@ from canvasapi.assignment import Assignment, AssignmentGroup, AssignmentOverride from canvasapi.blueprint import BlueprintSubscription from canvasapi.blueprint import BlueprintTemplate -from canvasapi.course import Course, CourseNickname, Page +from canvasapi.course import Course, CourseNickname, Page, LatePolicy from canvasapi.discussion_topic import DiscussionTopic from canvasapi.gradebook_history import ( Day, @@ -37,7 +37,7 @@ from canvasapi.outcome_import import OutcomeImport from canvasapi.paginated_list import PaginatedList from canvasapi.progress import Progress -from canvasapi.quiz import Quiz, QuizExtension +from canvasapi.quiz import Quiz, QuizExtension, QuizAssignmentOverrideSet from canvasapi.rubric import Rubric, RubricAssociation from canvasapi.section import Section from canvasapi.submission import GroupedSubmission, Submission @@ -308,6 +308,29 @@ def test_get_quiz_fail(self, m): with self.assertRaises(ResourceDoesNotExist): self.course.get_quiz(settings.INVALID_ID) + # get_quiz_overrides() + def test_get_quiz_overrides(self, m): + register_uris({"course": ["get_quiz_overrides"]}, m) + + overrides = self.course.get_quiz_overrides() + override_list = list(overrides) + + self.assertEqual(len(override_list), 2) + self.assertIsInstance(override_list[0], QuizAssignmentOverrideSet) + self.assertTrue(hasattr(override_list[0], "quiz_id")) + self.assertTrue(hasattr(override_list[0], "due_dates")) + self.assertTrue(hasattr(override_list[0], "all_dates")) + + attributes = ("id", "due_at", "unlock_at", "lock_at", "title", "base") + + self.assertTrue( + all(attribute in override_list[0].due_dates[0] for attribute in attributes) + ) + + self.assertTrue( + all(attribute in override_list[0].all_dates[0] for attribute in attributes) + ) + # get_quizzes() def test_get_quizzes(self, m): register_uris({"course": ["list_quizzes", "list_quizzes2"]}, m) @@ -464,7 +487,7 @@ def test_show_front_page(self, m): self.assertTrue(hasattr(front_page, "url")) self.assertTrue(hasattr(front_page, "title")) - # create_front_page() + # edit_front_page() def test_edit_front_page(self, m): register_uris({"course": ["edit_front_page"]}, m) @@ -474,6 +497,75 @@ def test_edit_front_page(self, m): self.assertTrue(hasattr(new_front_page, "url")) self.assertTrue(hasattr(new_front_page, "title")) + # edit_late_policy() + def test_edit_late_policy(self, m): + register_uris({"course": ["edit_late_policy"]}, m) + + late_policy_result = self.course.edit_late_policy( + late_policy={"missing_submission_deduction": 5} + ) + + self.assertTrue(late_policy_result) + + # get_late_policy + def test_get_late_policy(self, m): + register_uris({"course": ["get_late_policy"]}, m) + + late_policy = self.course.get_late_policy() + + self.assertIsInstance(late_policy, LatePolicy) + + attributes = ( + "id", + "course_id", + "missing_submission_deduction_enabled", + "missing_submission_deduction", + "late_submission_deduction_enabled", + "late_submission_deduction", + "late_submission_interval", + "late_submission_minimum_percent_enabled", + "late_submission_minimum_percent", + "created_at", + "updated_at", + ) + + for attribute in attributes: + self.assertTrue(hasattr(late_policy, attribute)) + + def test_create_late_policy(self, m): + register_uris({"course": ["create_late_policy"]}, m) + + late_policy = self.course.create_late_policy( + late_policy={ + "missing_submission_deduction_enabled": True, + "missing_submission_deduction": 12.34, + "late_submission_deduction_enabled": True, + "late_submission_deduction": 12.34, + "late_submission_interval": "hour", + "late_submission_minimum_percent_enabled": True, + "late_submission_minimum_percent": 12.34, + } + ) + + self.assertIsInstance(late_policy, LatePolicy) + + attributes = ( + "id", + "course_id", + "missing_submission_deduction_enabled", + "missing_submission_deduction", + "late_submission_deduction_enabled", + "late_submission_deduction", + "late_submission_interval", + "late_submission_minimum_percent_enabled", + "late_submission_minimum_percent", + "created_at", + "updated_at", + ) + + for attribute in attributes: + self.assertTrue(hasattr(late_policy, attribute)) + # get_page() def test_get_page(self, m): register_uris({"course": ["get_page"]}, m) @@ -1900,14 +1992,28 @@ def test_get_licenses(self, m): self.assertIsInstance(licenses, PaginatedList) licenses = list(licenses) - for l in licenses: - self.assertIsInstance(l, License) - self.assertTrue(hasattr(l, "id")) - self.assertTrue(hasattr(l, "name")) - self.assertTrue(hasattr(l, "url")) + for lic in licenses: + self.assertIsInstance(lic, License) + self.assertTrue(hasattr(lic, "id")) + self.assertTrue(hasattr(lic, "name")) + self.assertTrue(hasattr(lic, "url")) self.assertEqual(2, len(licenses)) + # resolve_path() + def test_resolve_path(self, m): + register_uris({"course": ["resolve_path"]}, m) + + full_path = "Folder_Level_1/Folder_Level_2/Folder_Level_3" + folders = self.course.resolve_path(full_path) + folder_list = [folder for folder in folders] + self.assertEqual(len(folder_list), 4) + self.assertIsInstance(folder_list[0], Folder) + for folder_name, folder in zip( + ("course_files/" + full_path).split("/"), folders + ): + self.assertEqual(folder_name, folder.name) + @requests_mock.Mocker() class TestCourseNickname(unittest.TestCase): @@ -1931,3 +2037,31 @@ def test_remove(self, m): self.assertIsInstance(deleted_nick, CourseNickname) self.assertTrue(hasattr(deleted_nick, "nickname")) + + +@requests_mock.Mocker() +class TestLatePolicy(unittest.TestCase): + def setUp(self): + self.canvas = Canvas(settings.BASE_URL, settings.API_KEY) + + self.late_policy = LatePolicy( + self.canvas._Canvas__requester, + { + "id": 123, + "course_id": 123, + "missing_submission_deduction_enabled": True, + "missing_submission_deduction": 12.34, + "late_submission_deduction_enabled": True, + "late_submission_deduction": 12.34, + "late_submission_interval": "hour", + "late_submission_minimum_percent_enabled": True, + "late_submission_minimum_percent": 12.34, + "created_at": "2012-07-01T23:59:00-06:00", + "updated_at": "2012-07-01T23:59:00-06:00", + }, + ) + + # __str__() + def test__str__(self, m): + string = str(self.late_policy) + self.assertIsInstance(string, str) diff --git a/tests/test_group.py b/tests/test_group.py index 1adb4897..ef61acc4 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -656,11 +656,11 @@ def test_get_licenses(self, m): self.assertIsInstance(licenses, PaginatedList) licenses = list(licenses) - for l in licenses: - self.assertIsInstance(l, License) - self.assertTrue(hasattr(l, "id")) - self.assertTrue(hasattr(l, "name")) - self.assertTrue(hasattr(l, "url")) + for lic in licenses: + self.assertIsInstance(lic, License) + self.assertTrue(hasattr(lic, "id")) + self.assertTrue(hasattr(lic, "name")) + self.assertTrue(hasattr(lic, "url")) self.assertEqual(2, len(licenses)) diff --git a/tests/test_quiz.py b/tests/test_quiz.py index 826e55e9..cf0b010d 100644 --- a/tests/test_quiz.py +++ b/tests/test_quiz.py @@ -9,17 +9,21 @@ from canvasapi.exceptions import RequiredFieldMissing from canvasapi.quiz import ( Quiz, + QuizStatistic, QuizSubmission, QuizSubmissionQuestion, QuizQuestion, QuizExtension, QuizSubmissionEvent, QuizReport, + QuizAssignmentOverrideSet, ) from canvasapi.quiz_group import QuizGroup from canvasapi.paginated_list import PaginatedList from tests import settings from tests.util import register_uris +from canvasapi.user import User +from canvasapi.submission import Submission @requests_mock.Mocker() @@ -296,24 +300,45 @@ def test_get_quiz_report(self, m): self.assertIsInstance(report, QuizReport) self.assertEqual(report.quiz_id, 1) + # get_quiz_report + def test_get_statistics(self, m): + register_uris({"quiz": ["get_statistics"]}, m) + + statistics = self.quiz.get_statistics() + + self.assertIsInstance(statistics, PaginatedList) + + statistic_list = [statistic for statistic in statistics] + + self.assertEqual(len(statistic_list), 1) + self.assertIsInstance(statistic_list[0], QuizStatistic) + self.assertEqual(statistic_list[0].id, "1") + self.assertTrue(hasattr(statistic_list[0], "question_statistics")) + self.assertEqual(len(statistic_list[0].question_statistics), 2) + # get_quiz_submission def test_get_quiz_submission(self, m): register_uris({"quiz": ["get_quiz_submission"]}, m) quiz_id = 1 - submission = self.quiz.get_quiz_submission(quiz_id) + quiz_submission = self.quiz.get_quiz_submission( + quiz_id, include=["quiz", "submission", "user"] + ) - self.assertIsInstance(submission, QuizSubmission) - self.assertTrue(hasattr(submission, "id")) - self.assertEqual(submission.quiz_id, quiz_id) - self.assertTrue(hasattr(submission, "quiz_version")) - self.assertEqual(submission.quiz_version, 1) - self.assertTrue(hasattr(submission, "user_id")) - self.assertEqual(submission.user_id, 1) - self.assertTrue(hasattr(submission, "validation_token")) - self.assertEqual(submission.validation_token, "this is a validation token") - self.assertTrue(hasattr(submission, "score")) - self.assertEqual(submission.score, 0) + self.assertIsInstance(quiz_submission, QuizSubmission) + self.assertTrue(hasattr(quiz_submission, "id")) + self.assertEqual(quiz_submission.quiz_id, quiz_id) + self.assertTrue(hasattr(quiz_submission, "quiz_version")) + self.assertEqual(quiz_submission.quiz_version, 1) + self.assertTrue(hasattr(quiz_submission, "user_id")) + self.assertEqual(quiz_submission.user_id, 1) + self.assertTrue(hasattr(quiz_submission, "validation_token")) + self.assertEqual(quiz_submission.validation_token, "this is a validation token") + self.assertTrue(hasattr(quiz_submission, "score")) + self.assertEqual(quiz_submission.score, 0) + self.assertIsInstance(quiz_submission.quiz, Quiz) + self.assertIsInstance(quiz_submission.submission, Submission) + self.assertIsInstance(quiz_submission.user, User) # create_submission def test_create_submission(self, m): @@ -595,6 +620,27 @@ def test_edit(self, m): self.assertEqual(self.question.question_name, question_dict["question_name"]) +@requests_mock.Mocker() +class TestQuizStatistic(unittest.TestCase): + def setUp(self): + self.canvas = Canvas(settings.BASE_URL, settings.API_KEY) + + with requests_mock.Mocker() as m: + register_uris( + {"course": ["get_by_id"], "quiz": ["get_by_id", "get_statistics"]}, m + ) + + self.course = self.canvas.get_course(1) + self.quiz = self.course.get_quiz(1) + self.quiz_statistics = self.quiz.get_statistics() + self.quiz_statistic = self.quiz_statistics[0] + + # __str__() + def test__str__(self, m): + string = str(self.quiz_statistic) + self.assertIsInstance(string, str) + + @requests_mock.Mocker() class TestQuizSubmissionEvent(unittest.TestCase): def setUp(self): @@ -692,3 +738,19 @@ def test_unflag_manual_validation_token(self, m): self.assertIsInstance(result, bool) self.assertTrue(result) self.assertFalse(self.submission_question.flagged) + + +@requests_mock.Mocker() +class TestQuizAssignmentOverrideSet(unittest.TestCase): + def setUp(self): + self.canvas = Canvas(settings.BASE_URL, settings.API_KEY) + + self.override_set = QuizAssignmentOverrideSet( + self.canvas._Canvas__requester, + {"quiz_id": "1", "due_dates": None, "all_dates": None}, + ) + + # __str__() + def test__str__(self, m): + string = str(self.override_set) + self.assertIsInstance(string, str) diff --git a/tests/test_requester.py b/tests/test_requester.py index bd17c116..3225f334 100644 --- a/tests/test_requester.py +++ b/tests/test_requester.py @@ -14,6 +14,7 @@ InvalidAccessToken, ResourceDoesNotExist, Unauthorized, + UnprocessableEntity, ) from tests import settings from tests.util import register_uris @@ -145,6 +146,12 @@ def test_request_409(self, m): with self.assertRaises(Conflict): self.requester.request("GET", "409") + def test_request_422(self, m): + register_uris({"requests": ["422"]}, m) + + with self.assertRaises(UnprocessableEntity): + self.requester.request("GET", "422") + def test_request_500(self, m): register_uris({"requests": ["500"]}, m) diff --git a/tests/test_user.py b/tests/test_user.py index bd6b3bb5..dc027d8e 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -617,11 +617,11 @@ def test_get_licenses(self, m): self.assertIsInstance(licenses, PaginatedList) licenses = list(licenses) - for l in licenses: - self.assertIsInstance(l, License) - self.assertTrue(hasattr(l, "id")) - self.assertTrue(hasattr(l, "name")) - self.assertTrue(hasattr(l, "url")) + for lic in licenses: + self.assertIsInstance(lic, License) + self.assertTrue(hasattr(lic, "id")) + self.assertTrue(hasattr(lic, "name")) + self.assertTrue(hasattr(lic, "url")) self.assertEqual(2, len(licenses)) diff --git a/tests/util.py b/tests/util.py index 64d7cee3..ef5dd708 100644 --- a/tests/util.py +++ b/tests/util.py @@ -4,11 +4,10 @@ import requests_mock -from canvasapi.util import get_institution_url from tests import settings -def register_uris(requirements, requests_mocker): +def register_uris(requirements, requests_mocker, base_url=None): """ Given a list of required fixtures and an requests_mocker object, register each fixture as a uri with the mocker. @@ -17,6 +16,8 @@ def register_uris(requirements, requests_mocker): :param requirements: dict :param requests_mocker: requests_mock.mocker.Mocker """ + if base_url is None: + base_url = settings.BASE_URL_WITH_VERSION for fixture, objects in requirements.items(): try: with open("tests/fixtures/{}.json".format(fixture)) as file: @@ -39,11 +40,7 @@ def register_uris(requirements, requests_mocker): if obj["endpoint"] == "ANY": url = requests_mock.ANY else: - url = ( - get_institution_url(settings.BASE_URL) - + "/api/v1/" - + obj["endpoint"] - ) + url = base_url + obj["endpoint"] try: requests_mocker.register_uri(