Repository: ucfopen/canvasapi (python)

Issue:
Updating Scores to a Specific Rubric
**What resource needs additional coverage?**
Rubrics

**What endpoints need to be covered?**
Requested here is a method that would allow one to upload scores on a specific rubric for a specific assignment. Ideally this would include the ability to add scores for multiple different criteria within the specified rubric. As @bennettscience noted in a discussion [here](https://github.com/ucfopen/canvasapi/discussions/537#discussioncomment-3132545), one can go directly through the Canvas API, but it would be nice to have a function to handle this from within directly through the canvasapi library itself.


Pull request description:
Issue 538 Rubric endpoint coverage
# Proposed Changes

Add endpoints for managing `RubricAssessments`.

Fixes #538.


Patch:
diff --git a/canvasapi/rubric.py b/canvasapi/rubric.py
index 5ee003f..e566cf4 100644
--- a/canvasapi/rubric.py
+++ b/canvasapi/rubric.py
@@ -26,10 +26,84 @@ class Rubric(CanvasObject):
         return Rubric(self._requester, response.json())
 
 
+class RubricAssessment(CanvasObject):
+    def __str__(self):
+        return "{}, {}".format(self.id, self.artifact_type)
+
+    def delete(self, **kwargs):
+        """
+        Delete a single RubricAssessment.
+
+        :calls: `DELETE /api/v1/courses/:course_id/rubric_associations\
+        /:rubric_association_id/rubric_assessments/:id \
+        <https://canvas.instructure.com/doc/api/rubrics.html#method.rubric_assessments.destroy>`_
+
+        :rtype: :class:`canvasapi.rubric.RubricAssessment`
+        """
+        from canvasapi.rubric import RubricAssessment
+
+        response = self._requester.request(
+            "DELETE",
+            "courses/{}/rubric_associations/{}/rubric_assessments/{}".format(
+                self.course_id, self.rubric_association_id, self.id
+            ),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        return RubricAssessment(self._requester, response.json())
+
+    def update(self, **kwargs):
+        """
+        Update a single RubricAssessment.
+
+        :calls: `PUT /api/v1/courses/:course_id/rubric_associations\
+        /:rubric_association_id/rubric_assessments/:id \
+        <https://canvas.instructure.com/doc/api/rubrics.html#method.rubric_assessments.update>`_
+
+        :rtype: :class:`canvasapi.rubric.RubricAssessment`
+        """
+        from canvasapi.rubric import RubricAssessment
+
+        response = self._requester.request(
+            "PUT",
+            "courses/{}/rubric_associations/{}/rubric_assessments/{}".format(
+                self.course_id, self.rubric_association_id, self.id
+            ),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        return RubricAssessment(self._requester, response.json())
+
+
 class RubricAssociation(CanvasObject):
     def __str__(self):
         return "{}, {}".format(self.id, self.association_type)
 
+    def create_rubric_assessment(self, **kwargs):
+        """
+        Create a single RubricAssessment.
+
+        :calls: `POST /api/v1/courses/:course_id/rubric_associations\
+        /:rubric_association_id/rubric_assessments \
+        <https://canvas.instructure.com/doc/api/rubrics.html#method.rubric_assessments.create>`_
+
+        :rtype: :class:`canvasapi.rubric.RubricAssessment`
+        """
+        from canvasapi.rubric import RubricAssessment
+
+        response = self._requester.request(
+            "POST",
+            "courses/{}/rubric_associations/{}/rubric_assessments".format(
+                self.course_id, self.id
+            ),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        assessment_json = response.json()
+        assessment_json.update({"course_id": self.id})
+
+        return RubricAssessment(self._requester, assessment_json)
+
     def delete(self, **kwargs):
         """
         Delete a RubricAssociation.
diff --git a/docs/rubric-ref.rst b/docs/rubric-ref.rst
index b02aaa2..14c36d3 100644
--- a/docs/rubric-ref.rst
+++ b/docs/rubric-ref.rst
@@ -5,6 +5,13 @@ Rubric
 .. autoclass:: canvasapi.rubric.Rubric
     :members:
 
+================
+RubricAssessment
+================
+
+.. autoclass:: canvasapi.rubric.RubricAssessment
+    :members:
+
 ==================
 Rubric Association
 ==================


Test patch:
diff --git a/tests/fixtures/rubric.json b/tests/fixtures/rubric.json
index df4e058..0eea19c 100644
--- a/tests/fixtures/rubric.json
+++ b/tests/fixtures/rubric.json
@@ -1,4 +1,64 @@
 {
+	"create_rubric_assessment": {
+		"method": "POST",
+		"endpoint": "courses/1/rubric_associations/4/rubric_assessments",
+		"data": {
+			"id": 1,
+			"rubric_id": 1,
+			"rubric_association_id": 4,
+			"score": 5.0,
+			"artifact_type": "Submission",
+			"artifact_id": 3,
+			"artifact_attempt": 2,
+			"assessment_type": "grading",
+			"assessor_id": 6,
+			"provisional": "false"
+		},
+		"status_code": 200
+	},
+	"update_rubric_assessment": {
+		"method": "PUT",
+		"endpoint": "courses/4/rubric_associations/4/rubric_assessments/1",
+		"data": {
+			"id": 1,
+			"rubric_id": 1,
+			"rubric_association_id": 4,
+			"score": 5.0,
+			"artifact_type": "Submission",
+			"artifact_id": 3,
+			"artifact_attempt": 2,
+			"assessment_type": "grading",
+			"assessor_id": 6,
+			"provisional": "true"
+		},
+		"status_code": 200
+	},
+	"delete_rubric_assessment": {
+		"method": "DELETE",
+		"endpoint": "courses/4/rubric_associations/4/rubric_assessments/1",
+		"data": {
+			"id": 1,
+			"rubric_id": 1,
+			"rubric_association_id": 4,
+			"score": 5.0,
+			"artifact_type": "Submission",
+			"artifact_id": 3,
+			"artifact_attempt": 2,
+			"assessment_type": "grading",
+			"assessor_id": 6,
+			"provisional": "false"
+		},
+		"status_code": 200
+	},
+	"create_rubric_association": {
+		"method": "POST",
+		"endpoint": "courses/1/rubric_associations/",
+		"data": {
+			"id": 5,
+			"association_type": "Assignment"
+		},
+		"status_code": 200
+	},
 	"update_rubric_association": {
 		"method": "PUT",
 		"endpoint": "courses/1/rubric_associations/4",
diff --git a/tests/test_rubric.py b/tests/test_rubric.py
index 6735225..90c09b7 100644
--- a/tests/test_rubric.py
+++ b/tests/test_rubric.py
@@ -3,7 +3,7 @@ import unittest
 import requests_mock
 
 from canvasapi import Canvas
-from canvasapi.rubric import Rubric, RubricAssociation
+from canvasapi.rubric import Rubric, RubricAssessment, RubricAssociation
 from tests import settings
 from tests.util import register_uris
 
@@ -59,6 +59,52 @@ class TestGradingStandard(unittest.TestCase):
         self.assertIsInstance(string, str)
 
 
+@requests_mock.Mocker()
+class TestRubricAssessment(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",
+                        "create_rubric_with_association",
+                        "create_rubric_association",
+                    ],
+                    "rubric": ["create_rubric_assessment"],
+                },
+                m,
+            )
+
+            self.course = self.canvas.get_course(1)
+            self.rubric = self.course.create_rubric()
+            self.association = self.course.create_rubric_association()
+            self.assessment = self.association.create_rubric_assessment()
+
+    def test__str__(self, m):
+        string = str(self.assessment)
+        self.assertIsInstance(string, str)
+
+    def test_update(self, m):
+        register_uris({"rubric": ["update_rubric_assessment"]}, m)
+
+        self.assertEqual(self.assessment.provisional, "false")
+
+        updated_assessment = self.assessment.update(provisional=True)
+
+        self.assertEqual(updated_assessment.id, 1)
+        self.assertEqual(updated_assessment.provisional, "true")
+
+    def test_delete(self, m):
+        register_uris({"rubric": ["delete_rubric_assessment"]}, m)
+
+        deleted_assessment = self.assessment.delete()
+
+        self.assertEqual(deleted_assessment.id, 1)
+        self.assertIsInstance(deleted_assessment, RubricAssessment)
+
+
 @requests_mock.Mocker()
 class TestRubricAssociation(unittest.TestCase):
     def setUp(self):