prompt: You will be provided with a partial code base and an issue statement explaining a problem to resolve.
  <issue>
  Locked questionnaires failing in editor form
  #1445 introduced locked questionnaires. However, they are not dealt with correctly in the evaluation editor form. When initially opening the form, the locked questionnaires are correctly selected but are not handled correctly when saving the form.
  
  Steps to reproduce:
  1. As manager, assign a locked questionnaire as the only general questionnaire for an evaluation.
  2. Enable the evaluation for editor review.
  3. As editor, open the evaluation form and try to save it. Saving will fail with an error for the field "General questionnaires" ("This field is required.").
  
  The locked questionnaire should count as a selected questionnaire and the form should be saved.
  A test should be added for this use case.
  
  </issue>
  <code>
  [start of evap/contributor/forms.py]
  1 from datetime import datetime, timedelta
  2 import logging
  3 
  4 from django import forms
  5 from django.conf import settings
  6 from django.db.models import Q
  7 from django.forms.widgets import CheckboxSelectMultiple
  8 from django.utils.translation import gettext_lazy as _
  9 from evap.evaluation.forms import UserModelMultipleChoiceField, UserModelChoiceField
  10 from evap.evaluation.models import Course, Evaluation, Questionnaire, UserProfile
  11 from evap.evaluation.tools import date_to_datetime
  12 from evap.staff.forms import ContributionForm
  13 
  14 logger = logging.getLogger(__name__)
  15 
  16 
  17 class EvaluationForm(forms.ModelForm):
  18     general_questionnaires = forms.ModelMultipleChoiceField(queryset=None, widget=CheckboxSelectMultiple, label=_("General questionnaires"))
  19     course = forms.ModelChoiceField(Course.objects.all(), disabled=True, required=False, widget=forms.HiddenInput())
  20     name_de_field = forms.CharField(label=_("Name (German)"), disabled=True, required=False)
  21     name_en_field = forms.CharField(label=_("Name (English)"), disabled=True, required=False)
  22 
  23     class Meta:
  24         model = Evaluation
  25         fields = ('name_de_field', 'name_en_field', 'vote_start_datetime', 'vote_end_date', 'general_questionnaires', 'course')
  26 
  27     def __init__(self, *args, **kwargs):
  28         super().__init__(*args, **kwargs)
  29 
  30         self.fields['name_de_field'].initial = self.instance.full_name_de
  31         self.fields['name_en_field'].initial = self.instance.full_name_en
  32 
  33         self.fields['general_questionnaires'].queryset = Questionnaire.objects.general_questionnaires().filter(
  34             Q(visibility=Questionnaire.Visibility.EDITORS) | Q(contributions__evaluation=self.instance)).distinct()
  35 
  36         self.fields['vote_start_datetime'].localize = True
  37         self.fields['vote_end_date'].localize = True
  38 
  39         if self.instance.general_contribution:
  40             self.fields['general_questionnaires'].initial = [q.pk for q in self.instance.general_contribution.questionnaires.all()]
  41 
  42         if not self.instance.allow_editors_to_edit:
  43             for field in self._meta.fields:
  44                 self.fields[field].disabled = True
  45 
  46     def clean(self):
  47         super().clean()
  48 
  49         vote_start_datetime = self.cleaned_data.get('vote_start_datetime')
  50         vote_end_date = self.cleaned_data.get('vote_end_date')
  51         if vote_start_datetime and vote_end_date:
  52             if vote_start_datetime.date() > vote_end_date:
  53                 self.add_error("vote_start_datetime", "")
  54                 self.add_error("vote_end_date", _("The first day of evaluation must be before the last one."))
  55 
  56     def clean_vote_end_date(self):
  57         vote_end_date = self.cleaned_data.get('vote_end_date')
  58 
  59         # The actual deadline is EVALUATION_END_OFFSET_HOURS:00 AM of the day after vote_end_date.
  60         # Therefore an evaluation date 24h + EVALUATION_END_OFFSET_HOURS in the past would technically still be in the future.
  61         if vote_end_date and date_to_datetime(vote_end_date) + timedelta(hours=24 + settings.EVALUATION_END_OFFSET_HOURS) < datetime.now():
  62             raise forms.ValidationError(_("The last day of evaluation must be in the future."))
  63         return vote_end_date
  64 
  65     def clean_general_questionnaires(self):
  66         # Ensure all locked questionnaires still have the same status (included or not)
  67         locked_qs = self.fields['general_questionnaires'].queryset.filter(is_locked=True)
  68 
  69         not_locked = [q for q in self.cleaned_data.get('general_questionnaires') if q not in locked_qs]
  70         locked = [q.pk for q in self.instance.general_contribution.questionnaires.filter(is_locked=True)]
  71 
  72         return not_locked + locked
  73 
  74     def save(self, *args, **kw):
  75         evaluation = super().save(*args, **kw)
  76         evaluation.general_contribution.questionnaires.set(self.cleaned_data.get('general_questionnaires'))
  77         return evaluation
  78 
  79 
  80 class EditorContributionForm(ContributionForm):
  81     def __init__(self, *args, **kwargs):
  82         super().__init__(*args, **kwargs)
  83 
  84         existing_contributor_pk = self.instance.contributor.pk if self.instance.contributor else None
  85 
  86         self.fields['questionnaires'].queryset = Questionnaire.objects.contributor_questionnaires().filter(
  87             Q(visibility=Questionnaire.Visibility.EDITORS) | Q(contributions__evaluation=self.evaluation)).distinct()
  88         self.fields['contributor'].queryset = UserProfile.objects.filter(
  89             (Q(is_active=True) & Q(is_proxy_user=False)) | Q(pk=existing_contributor_pk)
  90         )
  91 
  92 
  93 class DelegatesForm(forms.ModelForm):
  94     delegates = UserModelMultipleChoiceField(queryset=UserProfile.objects.exclude(is_active=False).exclude(is_proxy_user=True),
  95                                              required=False)
  96 
  97     class Meta:
  98         model = UserProfile
  99         fields = ('delegates',)
  100         field_classes = {
  101             'delegates': UserModelMultipleChoiceField,
  102         }
  103 
  104     def __init__(self, *args, **kwargs):
  105         super().__init__(*args, **kwargs)
  106 
  107     def save(self, *args, **kw):
  108         super().save(*args, **kw)
  109         logger.info('User "{}" edited the settings.'.format(self.instance.email))
  110 
  111 
  112 class DelegateSelectionForm(forms.Form):
  113     delegate_to = UserModelChoiceField(label=_("Delegate to"),
  114                                        queryset=UserProfile.objects.exclude(is_active=False).exclude(is_proxy_user=True))
  115
  [end of evap/contributor/forms.py]
  </code>
  
  I need you to solve this issue by generating a single patch file that I can apply directly to this repository using git apply. Please respond with a single patch file in the following format.
  <patch>
  diff --git a/file.py b/file.py
  --- a/file.py
  +++ b/file.py
  @@ -1,27 +1,35 @@
   def euclidean(a, b):
  -    while b:
  -        a, b = b, a % b
  -    return a
  +    if b == 0:
  +        return a
  +    return euclidean(b, a % b)
   
   
   def bresenham(x0, y0, x1, y1):
       points = []
       dx = abs(x1 - x0)
       dy = abs(y1 - y0)
  -    sx = 1 if x0 < x1 else -1
  -    sy = 1 if y0 < y1 else -1
  -    err = dx - dy
  +    x, y = x0, y0
  +    sx = -1 if x0 > x1 else 1
  +    sy = -1 if y0 > y1 else 1
   
  -    while True:
  -        points.append((x0, y0))
  -        if x0 == x1 and y0 == y1:
  -            break
  -        e2 = 2 * err
  -        if e2 > -dy:
  -            err -= dy
  -            x0 += sx
  -        if e2 < dx:
  -            err += dx
  -            y0 += sy
  +    if dx > dy:
  +        err = dx / 2.0
  +        while x != x1:
  +            points.append((x, y))
  +            err -= dy
  +            if err < 0:
  +                y += sy
  +                err += dx
  +            x += sx
  +    else:
  +        err = dy / 2.0
  +        while y != y1:
  +            points.append((x, y))
  +            err -= dx
  +            if err < 0:
  +                x += sx
  +                err += dy
  +            y += sy
  +
  +    points.append((x, y))
       return points
  
  </patch>
golden_diff: diff --git a/evap/contributor/forms.py b/evap/contributor/forms.py
  --- a/evap/contributor/forms.py
  +++ b/evap/contributor/forms.py
  @@ -15,7 +15,7 @@
   
   
   class EvaluationForm(forms.ModelForm):
  -    general_questionnaires = forms.ModelMultipleChoiceField(queryset=None, widget=CheckboxSelectMultiple, label=_("General questionnaires"))
  +    general_questionnaires = forms.ModelMultipleChoiceField(queryset=None, required=False, widget=CheckboxSelectMultiple, label=_("General questionnaires"))
       course = forms.ModelChoiceField(Course.objects.all(), disabled=True, required=False, widget=forms.HiddenInput())
       name_de_field = forms.CharField(label=_("Name (German)"), disabled=True, required=False)
       name_en_field = forms.CharField(label=_("Name (English)"), disabled=True, required=False)
  @@ -64,10 +64,14 @@
   
       def clean_general_questionnaires(self):
           # Ensure all locked questionnaires still have the same status (included or not)
  -        locked_qs = self.fields['general_questionnaires'].queryset.filter(is_locked=True)
  +        not_locked = []
  +        if self.cleaned_data.get('general_questionnaires'):
  +            not_locked = list(self.cleaned_data.get('general_questionnaires').filter(is_locked=False))
   
  -        not_locked = [q for q in self.cleaned_data.get('general_questionnaires') if q not in locked_qs]
  -        locked = [q.pk for q in self.instance.general_contribution.questionnaires.filter(is_locked=True)]
  +        locked = list(self.instance.general_contribution.questionnaires.filter(is_locked=True))
  +
  +        if not not_locked + locked:
  +            self.add_error("general_questionnaires", _("At least one questionnaire must be selected."))
   
           return not_locked + locked
  
verification_info: {"golden_diff": "diff --git a/evap/contributor/forms.py b/evap/contributor/forms.py\n--- a/evap/contributor/forms.py\n+++ b/evap/contributor/forms.py\n@@ -15,7 +15,7 @@\n \n \n class EvaluationForm(forms.ModelForm):\n-    general_questionnaires = forms.ModelMultipleChoiceField(queryset=None, widget=CheckboxSelectMultiple, label=_(\"General questionnaires\"))\n+    general_questionnaires = forms.ModelMultipleChoiceField(queryset=None, required=False, widget=CheckboxSelectMultiple, label=_(\"General questionnaires\"))\n     course = forms.ModelChoiceField(Course.objects.all(), disabled=True, required=False, widget=forms.HiddenInput())\n     name_de_field = forms.CharField(label=_(\"Name (German)\"), disabled=True, required=False)\n     name_en_field = forms.CharField(label=_(\"Name (English)\"), disabled=True, required=False)\n@@ -64,10 +64,14 @@\n \n     def clean_general_questionnaires(self):\n         # Ensure all locked questionnaires still have the same status (included or not)\n-        locked_qs = self.fields['general_questionnaires'].queryset.filter(is_locked=True)\n+        not_locked = []\n+        if self.cleaned_data.get('general_questionnaires'):\n+            not_locked = list(self.cleaned_data.get('general_questionnaires').filter(is_locked=False))\n \n-        not_locked = [q for q in self.cleaned_data.get('general_questionnaires') if q not in locked_qs]\n-        locked = [q.pk for q in self.instance.general_contribution.questionnaires.filter(is_locked=True)]\n+        locked = list(self.instance.general_contribution.questionnaires.filter(is_locked=True))\n+\n+        if not not_locked + locked:\n+            self.add_error(\"general_questionnaires\", _(\"At least one questionnaire must be selected.\"))\n \n         return not_locked + locked\n", "issue": "Locked questionnaires failing in editor form\n#1445 introduced locked questionnaires. However, they are not dealt with correctly in the evaluation editor form. When initially opening the form, the locked questionnaires are correctly selected but are not handled correctly when saving the form.\r\n\r\nSteps to reproduce:\r\n1. As manager, assign a locked questionnaire as the only general questionnaire for an evaluation.\r\n2. Enable the evaluation for editor review.\r\n3. As editor, open the evaluation form and try to save it. Saving will fail with an error for the field \"General questionnaires\" (\"This field is required.\").\r\n\r\nThe locked questionnaire should count as a selected questionnaire and the form should be saved.\r\nA test should be added for this use case.\n", "before_files": [{"content": "from datetime import datetime, timedelta\nimport logging\n\nfrom django import forms\nfrom django.conf import settings\nfrom django.db.models import Q\nfrom django.forms.widgets import CheckboxSelectMultiple\nfrom django.utils.translation import gettext_lazy as _\nfrom evap.evaluation.forms import UserModelMultipleChoiceField, UserModelChoiceField\nfrom evap.evaluation.models import Course, Evaluation, Questionnaire, UserProfile\nfrom evap.evaluation.tools import date_to_datetime\nfrom evap.staff.forms import ContributionForm\n\nlogger = logging.getLogger(__name__)\n\n\nclass EvaluationForm(forms.ModelForm):\n    general_questionnaires = forms.ModelMultipleChoiceField(queryset=None, widget=CheckboxSelectMultiple, label=_(\"General questionnaires\"))\n    course = forms.ModelChoiceField(Course.objects.all(), disabled=True, required=False, widget=forms.HiddenInput())\n    name_de_field = forms.CharField(label=_(\"Name (German)\"), disabled=True, required=False)\n    name_en_field = forms.CharField(label=_(\"Name (English)\"), disabled=True, required=False)\n\n    class Meta:\n        model = Evaluation\n        fields = ('name_de_field', 'name_en_field', 'vote_start_datetime', 'vote_end_date', 'general_questionnaires', 'course')\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n        self.fields['name_de_field'].initial = self.instance.full_name_de\n        self.fields['name_en_field'].initial = self.instance.full_name_en\n\n        self.fields['general_questionnaires'].queryset = Questionnaire.objects.general_questionnaires().filter(\n            Q(visibility=Questionnaire.Visibility.EDITORS) | Q(contributions__evaluation=self.instance)).distinct()\n\n        self.fields['vote_start_datetime'].localize = True\n        self.fields['vote_end_date'].localize = True\n\n        if self.instance.general_contribution:\n            self.fields['general_questionnaires'].initial = [q.pk for q in self.instance.general_contribution.questionnaires.all()]\n\n        if not self.instance.allow_editors_to_edit:\n            for field in self._meta.fields:\n                self.fields[field].disabled = True\n\n    def clean(self):\n        super().clean()\n\n        vote_start_datetime = self.cleaned_data.get('vote_start_datetime')\n        vote_end_date = self.cleaned_data.get('vote_end_date')\n        if vote_start_datetime and vote_end_date:\n            if vote_start_datetime.date() > vote_end_date:\n                self.add_error(\"vote_start_datetime\", \"\")\n                self.add_error(\"vote_end_date\", _(\"The first day of evaluation must be before the last one.\"))\n\n    def clean_vote_end_date(self):\n        vote_end_date = self.cleaned_data.get('vote_end_date')\n\n        # The actual deadline is EVALUATION_END_OFFSET_HOURS:00 AM of the day after vote_end_date.\n        # Therefore an evaluation date 24h + EVALUATION_END_OFFSET_HOURS in the past would technically still be in the future.\n        if vote_end_date and date_to_datetime(vote_end_date) + timedelta(hours=24 + settings.EVALUATION_END_OFFSET_HOURS) < datetime.now():\n            raise forms.ValidationError(_(\"The last day of evaluation must be in the future.\"))\n        return vote_end_date\n\n    def clean_general_questionnaires(self):\n        # Ensure all locked questionnaires still have the same status (included or not)\n        locked_qs = self.fields['general_questionnaires'].queryset.filter(is_locked=True)\n\n        not_locked = [q for q in self.cleaned_data.get('general_questionnaires') if q not in locked_qs]\n        locked = [q.pk for q in self.instance.general_contribution.questionnaires.filter(is_locked=True)]\n\n        return not_locked + locked\n\n    def save(self, *args, **kw):\n        evaluation = super().save(*args, **kw)\n        evaluation.general_contribution.questionnaires.set(self.cleaned_data.get('general_questionnaires'))\n        return evaluation\n\n\nclass EditorContributionForm(ContributionForm):\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n        existing_contributor_pk = self.instance.contributor.pk if self.instance.contributor else None\n\n        self.fields['questionnaires'].queryset = Questionnaire.objects.contributor_questionnaires().filter(\n            Q(visibility=Questionnaire.Visibility.EDITORS) | Q(contributions__evaluation=self.evaluation)).distinct()\n        self.fields['contributor'].queryset = UserProfile.objects.filter(\n            (Q(is_active=True) & Q(is_proxy_user=False)) | Q(pk=existing_contributor_pk)\n        )\n\n\nclass DelegatesForm(forms.ModelForm):\n    delegates = UserModelMultipleChoiceField(queryset=UserProfile.objects.exclude(is_active=False).exclude(is_proxy_user=True),\n                                             required=False)\n\n    class Meta:\n        model = UserProfile\n        fields = ('delegates',)\n        field_classes = {\n            'delegates': UserModelMultipleChoiceField,\n        }\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n    def save(self, *args, **kw):\n        super().save(*args, **kw)\n        logger.info('User \"{}\" edited the settings.'.format(self.instance.email))\n\n\nclass DelegateSelectionForm(forms.Form):\n    delegate_to = UserModelChoiceField(label=_(\"Delegate to\"),\n                                       queryset=UserProfile.objects.exclude(is_active=False).exclude(is_proxy_user=True))\n", "path": "evap/contributor/forms.py"}]}