Support all annotation types in add_annotations API (#7999)#8007
Conversation
b8d4b33 to
84fe165
Compare
Coverage Report for CI Build 27988288740Coverage increased (+0.03%) to 90.297%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats💛 - Coveralls |
| # matching the values reported by the annotations endpoint), mapped to the fields that must | ||
| # be present for each. Mirrors each subclass's model validations, enforced in add_annotations | ||
| # because insert_all bypasses them. | ||
| REQUIRED_ANNOTATION_FIELDS = { |
There was a problem hiding this comment.
Okay, so I think defining this is good, but really this information belongs in each model. It's a bit awkward to translate from string name to class, but since we're always deriving the expected class from the submission file anyway, I suggest the following changes:
- Modify
SubmissionFile.annotation_typeto beSubmissionFile.annotation_classand return the class itself (e.g.,TextAnnotation) rather than the string ("TextAnnotation"). - You can go from the class to its name with the
.namemethod - Define the symbol lists in each of the
Annotationsubclasses and then access them instead ofREQUIRED_ANNOTATION_FIELDS[expected_type]below - There's a use of
REQUIRED_ANNOTATION_FIELDS.key?below, but I don't think we need it. It's sufficient to check against the expect type and report the "does not match the type" error, without having a separate "Invalid annotation type" error.
There was a problem hiding this comment.
Great notes, thanks David. Fixing this
| annots = annotations_params | ||
| submission_files = result.submission.submission_files.index_by(&:filename) | ||
|
|
||
| # Validation pass: insert_all bypasses model validations, so guard the input here |
There was a problem hiding this comment.
A separate pass isn't necessary here. This check can happen inside the existing loop below; the early return will ensure nothing is inserted into the database, as that only happens after the loop
There was a problem hiding this comment.
I've moved validation into the main loop, but one of the specs in this PR (valid item creating a new category, followed by invalid item) started failing because find_or_create_by to insert annotation categories will commit as soon as it is called, thereby breaking atomicity and leaving an orphaned annotation category for that assignment.
To mitigate this, I've wrapped the entire loop in an ActiveRecord::Base.transaction that rolls back whenever any kind of error is caught.
| end | ||
| end | ||
|
|
||
| missing = REQUIRED_ANNOTATION_FIELDS[expected_type].select { |field| annot_params[field].blank? } |
There was a problem hiding this comment.
Let's be stricter here.
- If there are any required fields that are missing, report an error (this is what the previous
params.requirecall would have done) - If there are additional annotation fields that are "for the wrong type" (e.g. passing
line_startfor aPdfAnnotation) also report an error and return
Then, extract the annotation attributes (you can use the Ruby Hash#slice method and use that to create the hash that's appended to annotations, rather than passing all attributes.
|
|
||
| def annotations_params | ||
| params.require(annotations: [ | ||
| params.permit(annotations: [ |
There was a problem hiding this comment.
I'd like to keep the require, which was strict with the param requirement. I assume you changed this to allow the inner hashes to not have all of the attributes.
I think you can achieve both the strict :annotations key and the permissive inner structure by using the new expect method
There was a problem hiding this comment.
Oh nice! Yes, that's why it was changed. Fixing.
| annotation_type = submission_file.annotation_type | ||
| @annotation = result.annotations.create!( | ||
| type: annotation_type, | ||
| **type_specific_annotation_attributes(annotation_type), |
There was a problem hiding this comment.
Right, so this is the part where I'd like the logic to be the same for both controller methods.
- Get the list of symbols from a model method
- Call
Hash#sliceto get the values fromparams(no need to define a separate method)
… in add_annotations
…no extra unexpected fields)
e68da7d to
d110bbc
Compare
|
Thanks for the detailed review, David. I hope the new commits nail down your concerns. |
david-yz-liu
left a comment
There was a problem hiding this comment.
@philipkukulak this is great, thanks!
Proposed Changes
The API endpoint
POST add_annotationsnow supports all annotation types. Co-authored with Claude. Closes #7999.In achieving this, it was found that there was no validation of annotation type to file type for which we are seeking to annotate. This means a request to apply a
PdfAnnotationto a.pyfile would succeed, which would break rendering in the UI. To resolve this, some pre-existing type derivation logic was lifted out of the UIAnnotationsControllerand is now being used in both the UI and API controllers; the API also uses it as a part of the validation chain.As a side effect of this change,
add_existing_annotation's specs were lacking coverage due to a mix in record creation between annotation types:HtmlAnnotationusedcreate!, and all other annotations usedcreate. This is now consolidated. There was no coverage of the failure path for this record creation, so this was added here for good measure.Another small bug that was fixed is that prior to this change,
POSTing aTextAnnotationto a non-text file could create an annotation that would never be displayed on that file. Users will now see a422response if they try to do this.The API endpoint itself now either accepts an explicit annotation type, or derives it implicitly. In the former case, it expects a match between the annotation type and the file being annotated. In the latter, it simply reconciles it from the file being annotated.
Associated documentation repository pull request (if applicable)
MarkUsProject/Wiki#267
Type of Change
Checklist
Before opening your pull request:
After opening your pull request:
Questions and Comments
Here's a summary of the smoke tests for the changes to the endpoint:
Smoke test —
add_annotationsAPILocal server (
/csc108), authenticated instructor API key. Seeded one file per type on aversion-used submission:
hello.py,nemo.jpg,possimus.pdf,ipsa.ipynb. Each resultconfirmed by reading back through
GET …/groups/:id/annotations. Thetypefield uses thefull STI class names (
TextAnnotation/ImageAnnotation/PdfAnnotation/HtmlAnnotation).typesentTextAnnotationhello.pyTextAnnotationTextAnnotation, line/col persistedImageAnnotationnemo.jpgImageAnnotationImageAnnotation, x/y persistedPdfAnnotationpossimus.pdfPdfAnnotationPdfAnnotation, page 2HtmlAnnotationipsa.ipynbHtmlAnnotationHtmlAnnotation, nodes persistedpossimus.pdfPdfAnnotationPdfAnnotation, page 3TextAnnotationpossimus.pdfAnnotation type 'TextAnnotation' does not match the type of file 'possimus.pdf'bogushello.pyInvalid annotation type: boguspossimus.pdf(nopage)Missing required fields for PdfAnnotation: pageTextAnnotationnope.txtSubmission file not found: nope.txtTextAnnotation,TextAnnotationhello.py,possimus.pdfNet: all 5 valid writes persisted with the correct STI type on the correct file; all 4
bad-input paths failed closed with specific 422 messages and left the DB unchanged; batch
validation is atomic (rejects the whole request before any insert).