-
Notifications
You must be signed in to change notification settings - Fork 165
Separate questions and questionnaires #2513
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
f5218c6 to
7b87d12
Compare
This comment was marked as outdated.
This comment was marked as outdated.
91442b6 to
6602a7a
Compare
niklasmohrin
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.
Oh wow, this is quite the undertaking. I did not look too close at stuff yet, but here are some initial thoughts that I want to clear out first.
There are a number of changes in this PR that we could make independently (typing, refactors, added assertions), can you PR them in separately so that we clear some of the diff here? :D
6f80f10 to
61bec79
Compare
extracted some independent changes from #2513 for easier reviewability 1. pylint rule is covered in ruff and needed for out-of-line imports in temporary tool 1. question types helped me a lot to avoid question <-> questionassignment confusions 1. unrelated typo fix 1. fixed odd questionnaire type handling in text answer exporter (+ added type assertion) found while tracing question usage
0c0bfd3 to
9356904
Compare
9356904 to
94ca1d3
Compare
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
5f4e50d to
258bc8f
Compare
niklasmohrin
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.
didn't get far again, but here are some more thoughts :D
b324a0a to
fbb8163
Compare
richardebeling
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.
surprisingly little changes to production logic. Nice! I will have to look at migrations, tests, and test_data separately, but let's first agree on the actual implementation
niklasmohrin
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.
crossed off some more files :) I agree with Richard, maybe we can actually get this PR through quicker than I thought and before #2450. Let's see how we progress on the frontend, I think the backend is pretty solid 👍
|
I noticed a bug in the migrations (fixed in 82875a8): Somehow the unique constrained was removed after the migration. I suspect postgres drops the constraint with the column and django did not notice that. You can see that A when directly accessing the db, and B when you alter the unique together check in a new migration. |
e6c10d7 to
db268b9
Compare
|
Wait so what is the problem with the constraint being removed? Just that it is surprising, or does it actually result in database errors or so? Because the fix now is that we remove the constraint manually, which gives the result right? I feel that I didn't understand everything here :D |
first of all, there is no problem on main. Here, the question field is removed: EvaP/evap/evaluation/migrations/0158_unified_questions_from_tmp_relation.py Lines 50 to 53 in 82875a8
By removing the field, postgres also drops the unique constraint, but Django still thinks it is there. Altering the unique together in the same migration worked before, I think, because Django references the constraint before the migration is run (through schema editor): EvaP/evap/evaluation/migrations/0158_unified_questions_from_tmp_relation.py Lines 123 to 126 in 82875a8
|
|
Ah, okay, Django trips up. Makes sense, thanks! And good catch |
niklasmohrin
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.
Great stuff, I am very happy with the database side of this change and have 20/27 files checked off. All that remains is the form.
Some notes / questions:
evap/evaluation/migrations/0158_unified_questions_from_tmp_relation.py
Outdated
Show resolved
Hide resolved
bceedab to
456c647
Compare
niklasmohrin
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.
great stuff!
| type.tomselect.enable(); | ||
| allowTextanswer.disabled = false; | ||
| question_id.value = ""; | ||
| tooltip.disable(); |
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.
(commenting on somewhat random line of code) when loading the page and seeing the initial questions, the input elements are not disabled; they too should be disabled, or is it okay to edit those?
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 select elements are not disabled in editable questionnaires because the user can still change the question assignment. When selecting a different question / creating a new option, the existing question won't be modified if it occurs in another questionnaire.
Let's reopen this discussion at a later point and focus on the old UI in this PR
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 feel like we could maybe schedule a call with you, me, and @janno42 (and whoever else is interested) to make sure we all have the same final UI and behavior in mind. How do you feel about this? I think we have made good progress so far and I want to avoid stalling this PR with multiple review rounds about small details of this form
Things to discuss from my side:
- Should new questions be created via new entries in tomselect, or via a modal as we said some time ago?
- Given our n*m memory learnings, should we even use the current tomselect UI? Does it even make sense now that we never edit any individual field? Maybe a "select existing" modal with different search modes would be more intuitive?
- What should the implementation look like? Custom element? Typescript interfaces?
- Assuming we stay with the connected-tomselect approach, can we sensibly abstract this logic?
456c647 to
81fbb1c
Compare
keep the textarea based question form
81fbb1c to
eac8f4b
Compare
niklasmohrin
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.
didn't check everything yet, but what I saw looks good; the form part is definitely the most tricky, but it looks sound
| if self.instance.pk and self.instance.questionnaires.count() > 1 and self.has_changed(): | ||
| self.instance.pk = None # copy on write |
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.
Very clever :^)
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 this should be done in save() though
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.
Sounds good, then we can even leave self.instance unchanged I guess?
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.
Form.save basically calls just self.instance.save() if commit=True, otherwise users should save themselves, so we need to edit self.instance.
I tried setting self.instance.pk through cleaned_data, but that did not work
f03121c to
b1132eb
Compare
niklasmohrin
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.
Looking very good, I did another pass at checking of all easy files plus some remarks on the difficult parts. I think we are close to done :)
| {% endfor %} | ||
| <td>{% include 'bootstrap_form_field_widget.html' with field=form_element.question_form.text_de %}</td> | ||
| <td>{% include 'bootstrap_form_field_widget.html' with field=form_element.question_form.text_en %}</td> | ||
| <td class="question-type d-inline-block" data-toggle="tooltip"> |
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 used?
| <td class="question-type d-inline-block" data-toggle="tooltip"> | |
| <td class="question-type d-inline-block"> |
| <tbody> | ||
| {% for form_element in formset %} | ||
| {% include 'bootstrap_form_errors.html' with errors=form_element.non_field_errors %} | ||
| {% include 'bootstrap_form_errors.html' with errors=form_element.question_form.non_field_errors %} |
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.
(not a problem for this PR)
I think putting these error fields (and the hidden inputs above) into the table like this is illegal HTML. Not a problem, browser displays it correctly as far as I can tell
| def setUp(self): | ||
| super().setUp() |
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.
Why the change to instance setUp? I thought we only needed to do this for live tests?
|
|
||
| self.change_question().follow() | ||
|
|
||
| self.assertNotEqual(question, self.questionnaire.questions.get()) |
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.
Can we add something like self.assertEqual(question.text_en, "old text")? Just so that we are not running into any object identity vs. equality doubts here
| form["question_assignments-0-DELETE"] = "on" | ||
| form["question_assignments-0-type"].force_value(-1) | ||
| form.submit().follow() | ||
| self.assertQuerySetEqual(question.questionnaires.all(), []) |
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.
should we add a check here that ensures that the question is "garbage collected" if its last assignment is deleted? I guess it should check the number of questions present in the database
| kwargs.pop("instance") | ||
| self.question_form = QuestionForm(*args, instance=self.instance.question, **kwargs) |
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.
Could this give a KeyError on pop? Or do we expect to always have instance? If so, we could also change the signature to *args, instance, **kwargs and pass it along or not depending on the branch
| def clean(self) -> None: | ||
| super().clean() | ||
| if not self.question_form.is_valid(): | ||
| raise forms.ValidationError([]) |
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.
Wouldn't this add no errors to the error list? I feel like it should be something like
| raise forms.ValidationError([]) | |
| raise forms.ValidationError(_("Error validating question")) |
or even
| raise forms.ValidationError([]) | |
| raise forms.ValidationError(self.question_form.errors) |
What do you think?
| self.instance.question = self.question_form.save(commit) | ||
| return super().save(commit) |
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.
Can we do the following?
| self.instance.question = self.question_form.save(commit) | |
| return super().save(commit) | |
| self.instance.question = self.question_form.save(commit=commit) | |
| return super().save(commit=commit) |
close #457
featuring...
☑️ ~11 year old issue
☑️ >24k changed lines (nearly all in test data / tests though)
☑️ touch every file in the git tree
☑️ some non-trivial migrations
I await your comments or questions of questions with questions :)