Skip to content

Conversation

@Kakadus
Copy link
Collaborator

@Kakadus Kakadus commented Sep 24, 2025

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 :)

@Kakadus Kakadus force-pushed the combined-questions branch 2 times, most recently from f5218c6 to 7b87d12 Compare September 25, 2025 06:45
@Kakadus

This comment was marked as outdated.

@Kakadus Kakadus force-pushed the combined-questions branch 2 times, most recently from 91442b6 to 6602a7a Compare September 25, 2025 10:00
@niklasmohrin niklasmohrin self-requested a review October 14, 2025 20:59
Copy link
Member

@niklasmohrin niklasmohrin left a 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

@Kakadus Kakadus force-pushed the combined-questions branch 3 times, most recently from 6f80f10 to 61bec79 Compare October 20, 2025 19:03
janno42 pushed a commit that referenced this pull request Oct 25, 2025
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
@Kakadus Kakadus force-pushed the combined-questions branch 3 times, most recently from 0c0bfd3 to 9356904 Compare October 27, 2025 15:40
@Kakadus Kakadus force-pushed the combined-questions branch from 9356904 to 94ca1d3 Compare October 27, 2025 16:55
@Kakadus

This comment was marked as outdated.

@Kakadus

This comment was marked as outdated.

@Kakadus Kakadus requested review from niklasmohrin and removed request for niklasmohrin October 27, 2025 17:10
@Kakadus Kakadus force-pushed the combined-questions branch from 5f4e50d to 258bc8f Compare October 27, 2025 18:24
Copy link
Member

@niklasmohrin niklasmohrin left a 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

@Kakadus Kakadus force-pushed the combined-questions branch 2 times, most recently from b324a0a to fbb8163 Compare November 4, 2025 16:35
Copy link
Member

@richardebeling richardebeling left a 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

Copy link
Member

@niklasmohrin niklasmohrin left a 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 👍

@Kakadus
Copy link
Collaborator Author

Kakadus commented Nov 7, 2025

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.

@Kakadus Kakadus force-pushed the combined-questions branch 4 times, most recently from e6c10d7 to db268b9 Compare November 7, 2025 22:04
@niklasmohrin
Copy link
Member

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

@Kakadus
Copy link
Collaborator Author

Kakadus commented Nov 16, 2025

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.
We have a unique together on (question, contribution, answer).

Here, the question field is removed:

migrations.RemoveField(
model_name="ratinganswercounter",
name="question",
),

By removing the field, postgres also drops the unique constraint, but Django still thinks it is there.
When the unique together is altered, Django tries to reference the old constraint, which subsequently fails.

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):

migrations.AlterUniqueTogether(
name="ratinganswercounter",
unique_together={("assignment", "contribution", "answer")},
),

@niklasmohrin
Copy link
Member

niklasmohrin commented Nov 16, 2025

Ah, okay, Django trips up. Makes sense, thanks! And good catch

Copy link
Member

@niklasmohrin niklasmohrin left a 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:

@Kakadus Kakadus force-pushed the combined-questions branch 2 times, most recently from bceedab to 456c647 Compare November 22, 2025 11:36
Copy link
Member

@niklasmohrin niklasmohrin left a 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();
Copy link
Member

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?

Copy link
Collaborator Author

@Kakadus Kakadus Jan 4, 2026

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

Copy link
Member

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:

  1. Should new questions be created via new entries in tomselect, or via a modal as we said some time ago?
  2. 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?
  3. What should the implementation look like? Custom element? Typescript interfaces?
  4. Assuming we stay with the connected-tomselect approach, can we sensibly abstract this logic?

@Kakadus Kakadus force-pushed the combined-questions branch from 456c647 to 81fbb1c Compare January 4, 2026 12:45
keep the textarea based question form
Copy link
Member

@niklasmohrin niklasmohrin left a 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

Comment on lines +919 to +920
if self.instance.pk and self.instance.questionnaires.count() > 1 and self.has_changed():
self.instance.pk = None # copy on write
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very clever :^)

Copy link
Collaborator Author

@Kakadus Kakadus Jan 5, 2026

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

Copy link
Member

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?

Copy link
Collaborator Author

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

@Kakadus Kakadus force-pushed the combined-questions branch from f03121c to b1132eb Compare January 5, 2026 18:24
@Kakadus Kakadus requested a review from niklasmohrin January 5, 2026 18:39
Copy link
Member

@niklasmohrin niklasmohrin left a 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">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this used?

Suggested change
<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 %}
Copy link
Member

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

Comment on lines +3260 to +3261
def setUp(self):
super().setUp()
Copy link
Member

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())
Copy link
Member

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(), [])
Copy link
Member

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

Comment on lines +940 to +941
kwargs.pop("instance")
self.question_form = QuestionForm(*args, instance=self.instance.question, **kwargs)
Copy link
Member

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([])
Copy link
Member

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

Suggested change
raise forms.ValidationError([])
raise forms.ValidationError(_("Error validating question"))

or even

Suggested change
raise forms.ValidationError([])
raise forms.ValidationError(self.question_form.errors)

What do you think?

Comment on lines +955 to +956
self.instance.question = self.question_form.save(commit)
return super().save(commit)
Copy link
Member

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?

Suggested change
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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants