-
Notifications
You must be signed in to change notification settings - Fork 7
Feature/dnd validation #738
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #738 +/- ##
==========================================
+ Coverage 36.84% 38.10% +1.25%
==========================================
Files 540 543 +3
Lines 23642 23821 +179
Branches 2825 2848 +23
==========================================
+ Hits 8711 9077 +366
+ Misses 14056 13853 -203
- Partials 875 891 +16 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
This reverts commit 558eab8.
This fixes a bug where we didn't return a default explanation when an incorrect answer without an explanation was matched.
src/test/java/uk/ac/cam/cl/dtg/isaac/api/IsaacIntegrationTestWithREST.java
Fixed
Show fixed
Hide fixed
this is needed when a previous question attempt is available for a question
The goal is to have rules that validate the same field close to each other.
this is to bring this branch in sync with `main`
jsharkey13
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some initial thoughts.
One that is not addressed everywhere inline is the use of var. We have never used var before (because the codebase started in Java 6, and was Java 8 for most of its life). I would much prefer explicit types everywhere, since they are almost always easier to read (var can't even help us with the awful Map<String, ? extends Map<String, ? extends List<? extends LightweightQuestionValidationResponse>>> type we use a lot, since you obviously cannot use it in method signatures; that's one case where hiding this mess of a type would be useful and it's no help!). There's at least one comment on var with a suggestion from a Java style guide I found useful - since I hadn't met var before this PR.
|
|
||
| /** | ||
| * Choice for Dnd Questions, containing a list of DndItems. | ||
| * Choice for Dnd Questions, containing a list of DnDItems. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was this intentional?
| */ | ||
| @DTOMapping(DndChoiceDTO.class) | ||
| @JsonContentType("dndChoice") | ||
| public class DndChoice extends ItemChoice { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason for switching this hierarchy, given that DndItem still extends Item and we did not end up switching to a Map here as I had hoped?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(The DTO should have a parallel hierarchy, and matches this right now, but needs to if anything changes, too).
| return new DndValidationResponse(question.getId(), answer, isCorrect, dropZonesCorrect, feedback, new Date()); | ||
| } | ||
|
|
||
| private Optional<String> validate(final Question question, final Choice answer) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this is checking whether the question is valid, but I think since we use "validator" to mean "marker" exclusively across the codebase, we need another name here. Even something like getProblemsWithQuestionOrSubmission(...) is way clearer about what the purpose of this method is.
| } | ||
|
|
||
| /** Validate a set of rules. Each rule takes two arguments. `.add` some rules, then `.check` whether they held.*/ | ||
| public static class BiRuleValidator<T, U> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These methods are again about checking whether content is valid, which is not what we use "validator" to mean across the codebase. They do not belong in isaac.quiz, certainly.
For now, maybe making them live in a new package called isaac.util, in ContentValidatorUtils to suchlike? I think with extra context, we can keep "validator" in the name and maybe we'll refactor the validators to be called markers at some point?
| return injector; | ||
| } | ||
|
|
||
| public static void setInjector(final Injector newInjector) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is dangerous, and I don't like it. If we add it, we should add it @Deprecated immediately, so that nobody thinks they can use it. It also needs either a name or clear JavaDoc indicating the dangers of using it, i.e. that it will pollute the global creation of new classes going forwards.
If this is used in a test, all subsequent tests may be affected.
| var feedback = (Content) matchedAnswer.map(Choice::getExplanation) | ||
| .or(() -> !isCorrect && answer.getItems().size() < closestCorrect.map(c -> c.getItems().size()).orElse(0) | ||
| ? Optional.of(new Content(FEEDBACK_ANSWER_NOT_ENOUGH)) : Optional.empty()) | ||
| .or(() -> !isCorrect ? Optional.ofNullable(question.getDefaultFeedback()) : Optional.empty()) | ||
| .orElse(null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really hard to read. It is a complete mess of nested streams and Optionals, I think, but I can't actually tell. Maybe it's actually all Optionals?
This would be so much easier to read with some intermediate variables, and maybe is a use-case for var (though I don't think var vs boolean is much of a saving!).
| @SuppressWarnings("checkstyle:MissingJavadocType") | ||
| public static class QuestionHelpers { | ||
| public static Stream<DndChoice> getChoices(final IsaacDndQuestion question) { | ||
| return question.getChoices().stream().map(c -> (DndChoice) c); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this not an unchecked cast? I know that as written, your code does some checking of the types before this is ever called, but that's not enforced by anything and it's not at all obvious it is happening.
| var feedback = (Content) matchedAnswer.map(Choice::getExplanation) | ||
| .or(() -> !isCorrect && answer.getItems().size() < closestCorrect.map(c -> c.getItems().size()).orElse(0) | ||
| ? Optional.of(new Content(FEEDBACK_ANSWER_NOT_ENOUGH)) : Optional.empty()) | ||
| .or(() -> !isCorrect ? Optional.ofNullable(question.getDefaultFeedback()) : Optional.empty()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we use default feedback even if the answer is correct, provided there is no other feedback already being given to the user.
I.e. if the matched correct choice has feedback, we use that (check with feedbackIsNullOrEmpty because every choice the editor creates has an empty explanation but that's not the same as having feedback), otherwise we still use default feedback.
|
|
||
| private static class ChoiceHelpers { | ||
| public static boolean matches(final DndChoice lhs, final DndChoice rhs) { | ||
| return lhs.getItems().stream().allMatch(lhsItem -> dropZoneEql(rhs, lhsItem)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Won't this NullPointerException if getItems() is null? All of these helpers need null guards, no? Whereas if they were inlined we could be sure we'd already done the null check?
| .collect(Collectors.toList()); | ||
| } | ||
|
|
||
| private static Stream<String> getDropZonesFromContent(final ContentBase content) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like the fact that we use this method at runtime on every single question attempt. I can see why it may be worth having for the content errors page, and that you want to make the feedback given to the user be consistent with this - but it should be unnecessary almost all of the time.
I wouldn't object to doing a runtime check for DEV mode and only validating it then (much like we do elsewhere already- though this uses a lookup from properties, we sometimes cache it for faster lookup), but ETL runs in PROD so using the same method for ETL and runtime checking will be a pain.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is one of my biggest complaints about the "small functions abound" style; it's really hard to reason about what we're doing when, because this method is so far down a convoluted call stack from the main validateQuestionResponse it is hard to tell as a human reading the code. IntelliJ is clever enough to detect it at least.
jsharkey13
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some initial thoughts.
One that is not addressed everywhere inline is the use of var. We have never used var before (because the codebase started in Java 6, and was Java 8 for most of its life). I would much prefer explicit types everywhere, since they are almost always easier to read (var can't even help us with the awful Map<String, ? extends Map<String, ? extends List<? extends LightweightQuestionValidationResponse>>> type we use a lot, since you obviously cannot use it in method signatures; that's one case where hiding this mess of a type would be useful and it's no help!). There's at least one comment on var with a suggestion from a Java style guide I found useful - since I hadn't met var before this PR.
This adds a validator for the DnD question type. You can test that this works using the following question: http://localhost:8004/questions/c4520e54-4777-41b9-9967-2893cea96a62. Although the front-end does not yet
implement showing item-level feedback, you can see that the response also contains the necessary information for this.
The feature
Here's how it works. To create a drag-and-drop question, the content developer must specify the type as drag and drop, declare drop zones in either text or figure content, and define the drag-and-drop items. Then, they need to specify at least one correct answer, which is a pairing of drop zones and items. They may also define incorrect answers. This is only necessary if they'd like to show specific feedback to the student about why a submission is wrong.
Requirements for answers:
Differences between DnD and Cloze questions.
The implementation
There are changes to 13 non-test and 7 test files.
Code changes
(15 files)8 files):DndChoice,DndChoiceDTO,DndValidationResponse,DndValidationResponseDTO. The validator forIsaacDndQuestionneeded a minor adjustment, and the change toItemChoiceis stylistic.QuestionValidationResponseDeserializerneeded changing so we correctly preload DnD questions with the last attempt when one is available.QuestionValidationMappersupports the newDndValidationResponsetype.IsaacDndValidatorandValidationUtils(2 files). This is where validation is mainly implemented. I've modelled the behavior onIsaacClozeValidator. Compared to it, the most important changes are:IsaacClozeValidatortries to fix invalid questions by, for example, ignoring any answers that are invalid (eg: the item id is missing), IsaacDndValidator refuses to validate invalid questions (it says the answer was incorrect, and explains that the question is invalid). I'm hoping that by being stricter with validation, we'll save ourselves some trouble later, by only needing to support a more limited number of question states for this type. Invalid DnD questions show up as non-critical content errors. Even when invalid, they're ETL'ed.SegueGuiceConfigurationModule: a newsetInjectormethod, used for mocking the injector from integration tests. It would be better if we didn't have to mock at all from integration tests, but I didn't have the strength to make this change.ElasticSearchIndexer: changed a few method visisbilites to public so we can load content objects during integration testsContentIndexerimplements the ETL process. This has been updated so invalid questions show up as content errors. This performs the same question validation as the marking endpoint.Test changes
QuestionsFacade. I made this because I wanted to explore how Jackson reacts to requests with invalid JSON, to get a better idea of what still needed to be checked manually in the validator. I needed to extendAbstractIntegrationTestso it can provide the new dependencies this required.IsaacIntegrationTestWithRestTestAppenderhelper, which I used when testing loggingIsaacDndValidator, and extended tests forContentIndexer.IsaacClozeValidatoralso got a new test, despite the functionality in the class not getting modified (I wanted to be sure it worked how I expected it to).Logging
When the
/answerendpoint encounters an invalid question, it logs this so we are aware there is an invalid question on the platform. However, it doesn't create logs for attempts that are generated by users. The ETL process also doesn't log invalid questions, as these are just recorded as content errors.Security
After reading through the code of several validators, I concluded that protecting against SQL injuctions doesn't happen here. I did add stricter validation, which should also make the endpoint more secure, but didn't add any additional protections against SQL injection to the DnD validator (eg: by sanitising the submitted answer). Although the validator itself never touches the database, the validation response it produces does get recorded, and the response includes the answer, which is user-provided. If the reviewer feels I should have done this as part of this PR, I'm happy to do it.
Performance
Possible improvements
Please don't be afraid to request changes, I'm sure we can find a way to get them done quickly^^.
Pull Request Check List