internlm/SWE-Fixer-Train-110K huggingface dataset:
Repository: e-valuation/EvaP

Issue:
Participant count wrong for unpublished results
On the results index page the number of voters and participants for unpublished evaluations is not shown correctly. Staff users can see these evaluations before they are published, but the numbers differ from the actual values (which are shown correctly on results detail pages).


Discussion:

Resolving patch:
diff --git a/evap/results/views.py b/evap/results/views.py
--- a/evap/results/views.py
+++ b/evap/results/views.py
@@ -107,7 +107,7 @@ def get_evaluations_with_prefetched_data(evaluations):
                 "course__degrees",
                 "course__semester",
                 "course__responsibles",
-            )
+            ).order_by('pk')
         )
         for evaluation, participant_count, voter_count, course_evaluations_count in zip(evaluations, participant_counts, voter_counts, course_evaluations_counts):
             if evaluation._participant_count is None:


Test patch:
Repository: e-valuation/EvaP

Issue:
Changing a username won’t refresh results cache
When the name of a contributor is changed, the new name won’t be reflected on the results pages.


Discussion:

Resolving patch:
diff --git a/evap/staff/forms.py b/evap/staff/forms.py
--- a/evap/staff/forms.py
+++ b/evap/staff/forms.py
@@ -15,6 +15,7 @@
                                     FaqSection, Question, Questionnaire, RatingAnswerCounter, Semester, TextAnswer,
                                     UserProfile)
 from evap.evaluation.tools import date_to_datetime
+from evap.results.tools import collect_results
 from evap.results.views import (update_template_cache,
                                 update_template_cache_of_published_evaluations_in_course)
 
@@ -758,6 +759,11 @@ def save(self, *args, **kw):
 
         self.instance.is_active = not self.cleaned_data.get('is_inactive')
 
+        # refresh results cache
+        for evaluation in Evaluation.objects.filter(contributions__contributor=self.instance).distinct():
+            if any(attribute in self.changed_data for attribute in ["first_name", "last_name", "title"]):
+                collect_results(evaluation, force_recalculation=True)
+
         self.instance.save()
 
 


Test patch:
diff --git a/evap/staff/tests/test_forms.py b/evap/staff/tests/test_forms.py
--- a/evap/staff/tests/test_forms.py
+++ b/evap/staff/tests/test_forms.py
@@ -9,6 +9,7 @@
                                          to_querydict)
 from evap.staff.forms import (ContributionForm, ContributionFormSet, CourseForm, EvaluationEmailForm, EvaluationForm,
                               QuestionnaireForm, SingleResultForm, UserForm)
+from evap.results.tools import collect_results
 from evap.contributor.forms import EvaluationForm as ContributorEvaluationForm
 
 
@@ -143,6 +144,31 @@ def test_user_cannot_be_removed_from_evaluation_already_voted_for(self):
         self.assertIn('evaluations_participating_in', form.errors)
         self.assertIn("Evaluations for which the user already voted can't be removed", form.errors['evaluations_participating_in'][0])
 
+    def test_results_cache_refreshed(self):
+        contributor = baker.make(UserProfile, first_name="Peter")
+        evaluation = baker.make(Evaluation, state="published")
+        baker.make(Contribution, contributor=contributor,
+                   evaluation=evaluation)
+
+        results_before = collect_results(evaluation)
+
+        form_data = get_form_data_from_instance(UserForm, contributor)
+        form_data["first_name"] = "Patrick"
+        form = UserForm(form_data, instance=contributor)
+        form.save()
+
+        results_after = collect_results(evaluation)
+
+        self.assertEqual(
+            results_before.contribution_results[0].contributor.first_name,
+            "Peter"
+        )
+
+        self.assertEqual(
+            results_after.contribution_results[0].contributor.first_name,
+            "Patrick"
+        )
+
 
 class SingleResultFormTests(TestCase):
     def test_single_result_form_saves_participant_and_voter_count(self):
Repository: e-valuation/EvaP

Issue:
OpenID login button is broken in latest firefox
Behavior:
When clicking the "login" button at `https://evap.hpi.de` a request is sent to `https://evap.hpi.de/oidc/authenticate/`, which returns a 302 and redirects to `https://evap.hpi.de/?next=/oidc/authenticate/`, which has no visible difference with `https://evap.hpi.de`. After clicking the login button again I'm redirected to `https://evap.hpi.de/?next=/oidc/authenticate/%3Fnext%3D/oidc/authenticate/`

Expected Behavior:
Display the openID page and allow login

Steps to reproduce
Go to https://evap.hpi.de and click the "login" button. Browser is an up-to-date firefox on linux


Discussion:

Resolving patch:
diff --git a/evap/middleware.py b/evap/middleware.py
--- a/evap/middleware.py
+++ b/evap/middleware.py
@@ -17,6 +17,9 @@ def process_view(request, view_func, _view_args, _view_kwargs):
         if "no_login_required" in view_func.__dict__ and view_func.no_login_required:
             return None
 
+        if view_func.__name__ in ["OIDCAuthenticationRequestView", "OIDCAuthenticationCallbackView"]:
+            return None
+
         return redirect_to_login(request.get_full_path())
 
 


Test patch:
Repository: e-valuation/EvaP

Issue:
Importer string cleaning
All importers should replace multiple consecutive spaces in cell values by single spaces.

There should be a test in which an exemplary enrollment file is imported, which includes two courses with a name differing only in the amount of spaces. The test should assert that only one course is created by the import.


Discussion:

Resolving patch:
diff --git a/evap/staff/importers.py b/evap/staff/importers.py
--- a/evap/staff/importers.py
+++ b/evap/staff/importers.py
@@ -244,7 +244,8 @@ def for_each_row_in_excel_file_do(self, row_function):
         for sheet in self.book.sheets():
             try:
                 for row in range(self.skip_first_n_rows, sheet.nrows):
-                    row_function(sheet.row_values(row), sheet, row)
+                    # see https://stackoverflow.com/questions/2077897/substitute-multiple-whitespace-with-single-whitespace-in-python
+                    row_function([' '.join(cell.split()) for cell in sheet.row_values(row)], sheet, row)
                 self.success_messages.append(_("Successfully read sheet '%s'.") % sheet.name)
             except Exception:
                 self.warnings[ImporterWarning.GENERAL].append(


Test patch:
diff --git a/evap/staff/fixtures/test_enrollment_data_consecutive_and_trailing_spaces.xls b/evap/staff/fixtures/test_enrollment_data_consecutive_and_trailing_spaces.xls
new file mode 100644
Binary files /dev/null and b/evap/staff/fixtures/test_enrollment_data_consecutive_and_trailing_spaces.xls differ
diff --git a/evap/staff/tests/test_importers.py b/evap/staff/tests/test_importers.py
--- a/evap/staff/tests/test_importers.py
+++ b/evap/staff/tests/test_importers.py
@@ -125,6 +125,7 @@ class TestEnrollmentImporter(TestCase):
     filename_valid = os.path.join(settings.BASE_DIR, "staff/fixtures/test_enrollment_data.xls")
     filename_valid_degree_merge = os.path.join(settings.BASE_DIR, "staff/fixtures/test_enrollment_data_degree_merge.xls")
     filename_valid_import_names = os.path.join(settings.BASE_DIR, "staff/fixtures/test_enrollment_data_import_names.xls")
+    filename_valid_consecutive_and_trailing_spaces = os.path.join(settings.BASE_DIR, "staff/fixtures/test_enrollment_data_consecutive_and_trailing_spaces.xls")
     filename_invalid = os.path.join(settings.BASE_DIR, "staff/fixtures/invalid_enrollment_data.xls")
     filename_random = os.path.join(settings.BASE_DIR, "staff/fixtures/random.random")
 
@@ -278,6 +279,12 @@ def test_duplicate_course_error(self):
             "Course Stehlen does already exist in this semester.",
             "Course Shine does already exist in this semester."})
 
+    def test_replace_consecutive_and_trailing_spaces(self):
+        with open(self.filename_valid_consecutive_and_trailing_spaces, "rb") as excel_file:
+            excel_content = excel_file.read()
+        success_messages, __, __ = EnrollmentImporter.process(excel_content, self.semester, None, None, test_run=True)
+        self.assertIn("The import run will create 1 courses/evaluations and 3 users", "".join(success_messages))
+
 
 class TestPersonImporter(TestCase):
     @classmethod
Repository: e-valuation/EvaP

Issue:
Proxied evaluations shouldn't show as delegated
Evaluations where a proxy user is responsible shouldn't be shown as "delegated courses" for the delegates of this proxy user. The delegates should see this course in the list of their own courses like those they are directly responsible for and they shouldn't be hidden when the option to hide delegated courses is active.


Discussion:

Resolving patch:
diff --git a/evap/contributor/views.py b/evap/contributor/views.py
--- a/evap/contributor/views.py
+++ b/evap/contributor/views.py
@@ -24,11 +24,14 @@ def index(request):
     user = request.user
     show_delegated = get_parameter_from_url_or_session(request, "show_delegated", True)
 
+    represented_proxy_users = user.represented_users.filter(is_proxy_user=True)
     contributor_visible_states = ['prepared', 'editor_approved', 'approved', 'in_evaluation', 'evaluated', 'reviewed', 'published']
     own_courses = Course.objects.filter(
         Q(evaluations__state__in=contributor_visible_states) & (
             Q(responsibles=user) |
-            Q(evaluations__contributions__contributor=user)
+            Q(evaluations__contributions__contributor=user) |
+            Q(evaluations__contributions__contributor__in=represented_proxy_users) |
+            Q(responsibles__in=represented_proxy_users)
         )
     )
     own_evaluations = [evaluation for course in own_courses for evaluation in course.evaluations.all() if evaluation.can_be_seen_by(user)]
@@ -37,7 +40,7 @@ def index(request):
 
     displayed_evaluations = set(own_evaluations)
     if show_delegated:
-        represented_users = user.represented_users.all()
+        represented_users = user.represented_users.exclude(is_proxy_user=True)
         delegated_courses = Course.objects.filter(
             Q(evaluations__state__in=contributor_visible_states) & (
                 Q(responsibles__in=represented_users) |


Test patch:
Repository: e-valuation/EvaP

Issue:
Access denied on manager login
Currently, after logging in, a manager is redirected to /staff/, but staff mode will not be active, so they will get a 403 access denied.

@janno42 what behavior do we want here? Redirect as if they weren't a manager or enable staff mode?


Discussion:
They should be redirected as if they weren't manager (or reviewer).

Resolving patch:
diff --git a/evap/evaluation/views.py b/evap/evaluation/views.py
--- a/evap/evaluation/views.py
+++ b/evap/evaluation/views.py
@@ -74,6 +74,7 @@ def index(request):
             # clean up our test cookie
             if request.session.test_cookie_worked():
                 request.session.delete_test_cookie()
+            return redirect('evaluation:index')
 
     # if not logged in by now, render form
     if not request.user.is_authenticated:


Test patch:
Repository: e-valuation/EvaP

Issue:
Write test for the login workflow
https://github.com/e-valuation/EvaP/pull/1531 should've been caught by a test. While at it, verify there's a test for logging in via a login URL.


Discussion:

Resolving patch:
diff --git a/evap/settings.py b/evap/settings.py
--- a/evap/settings.py
+++ b/evap/settings.py
@@ -262,8 +262,8 @@
 
 WSGI_APPLICATION = 'evap.wsgi.application'
 
-# Redirect url after login
 LOGIN_REDIRECT_URL = '/'
+LOGOUT_REDIRECT_URL = '/'
 
 LOGIN_URL = "/"
 
@@ -371,7 +371,6 @@
 OIDC_RP_SIGN_ALGO = 'RS256'
 OIDC_USERNAME_ALGO = ''
 OIDC_RP_SCOPES = 'openid email profile'
-LOGOUT_REDIRECT_URL = '/'
 
 OIDC_RP_CLIENT_ID = 'evap'
 OIDC_RP_CLIENT_SECRET = 'evap-secret'
diff --git a/evap/staff/views.py b/evap/staff/views.py
--- a/evap/staff/views.py
+++ b/evap/staff/views.py
@@ -1771,7 +1771,7 @@ def export_contributor_results_view(request, contributor_id):
 def enter_staff_mode(request):
     staff_mode.enter_staff_mode(request)
     messages.success(request, _("Successfully entered staff mode."))
-    return redirect('/')
+    return redirect('evaluation:index')
 
 
 @require_POST
@@ -1779,4 +1779,4 @@ def enter_staff_mode(request):
 def exit_staff_mode(request):
     staff_mode.exit_staff_mode(request)
     messages.success(request, _("Successfully exited staff mode."))
-    return redirect('/')
+    return redirect('evaluation:index')


Test patch:
diff --git a/evap/evaluation/tests/test_views.py b/evap/evaluation/tests/test_views.py
--- a/evap/evaluation/tests/test_views.py
+++ b/evap/evaluation/tests/test_views.py
@@ -1,5 +1,6 @@
 from django.core import mail
 from django.contrib.auth.hashers import make_password
+from django.contrib.auth.models import Group
 
 from django_webtest import WebTest
 from model_bakery import baker
@@ -22,6 +23,19 @@ def test_passworduser_login(self):
         password_form['password'] = 'evap'
         self.assertEqual(password_form.submit().status_code, 302)
 
+    def test_login_for_staff_users_correctly_redirects(self):
+        """ Regression test for #1523: Access denied on manager login """
+        internal_email = 'manager@institution.example.com'  # external users don't necessarily have a proper redirect page
+        baker.make(UserProfile, email=internal_email, password=make_password('evap'), groups=[Group.objects.get(name='Manager')])
+
+        response = self.app.get(self.url)
+        password_form = response.forms[0]
+        password_form['email'] = internal_email
+        password_form['password'] = 'evap'
+        response = password_form.submit()
+        self.assertRedirects(response, self.url, fetch_redirect_response=False)
+        self.assertRedirects(response.follow(), '/results/')
+
     def test_send_new_login_key(self):
         """ Tests whether requesting a new login key is only possible for existing users,
             shows the expected success message and sends only one email to the requesting
Repository: e-valuation/EvaP

Issue:
Show available variables for email templates
Staff users should see a list of available variables that can be used when editing the email templates.


Discussion:
yeah definitely. i would make them more copypasteable by including the {% %} tags or maybe even adding a "copy to clipboard" button alongside the variables that adds those tags. maybe we could also give the user a snippet for the loop through the courses this way.

Resolving patch:
diff --git a/evap/staff/views.py b/evap/staff/views.py
--- a/evap/staff/views.py
+++ b/evap/staff/views.py
@@ -1706,7 +1706,26 @@ def template_edit(request, template_id):
         messages.success(request, _("Successfully updated template."))
         return redirect('staff:index')
 
-    return render(request, "staff_template_form.html", dict(form=form, template=template))
+    available_variables = [
+        "contact_email",
+        "page_url",
+        "login_url",        # only if they need it
+        "user",
+    ]
+
+    if template.name == EmailTemplate.STUDENT_REMINDER:
+        available_variables += ["first_due_in_days", "due_evaluations"]
+    elif template.name in [EmailTemplate.PUBLISHING_NOTICE_CONTRIBUTOR, EmailTemplate.PUBLISHING_NOTICE_PARTICIPANT, EmailTemplate.EDITOR_REVIEW_NOTICE, EmailTemplate.EDITOR_REVIEW_REMINDER]:
+        available_variables += ["evaluations"]
+    elif template.name == EmailTemplate.EVALUATION_STARTED:
+        available_variables += ["evaluations", "due_evaluations"]
+    elif template.name == EmailTemplate.DIRECT_DELEGATION:
+        available_variables += ["evaluation", "delegate_user"]
+
+    available_variables = ["{{ " + variable + " }}" for variable in available_variables]
+    available_variables.sort()
+
+    return render(request, "staff_template_form.html", dict(form=form, template=template, available_variables=available_variables))
 
 
 @manager_required


Test patch:
Repository: e-valuation/EvaP

Issue:
Set reply-to header
For all emails sent via one of the contact modals, the reply-to header should be set to the address of the person sending the request.


Discussion:

Resolving patch:
diff --git a/evap/evaluation/views.py b/evap/evaluation/views.py
--- a/evap/evaluation/views.py
+++ b/evap/evaluation/views.py
@@ -163,7 +163,8 @@ def contact(request):
         mail = EmailMessage(
             subject=subject,
             body="{}\n{}\n\n{}".format(title, request.user.email, message),
-            to=[settings.CONTACT_EMAIL])
+            to=[settings.CONTACT_EMAIL],
+            reply_to=[request.user.email])
         try:
             mail.send()
             logger.info('Sent contact email: \n{}\n'.format(mail.message()))


Test patch:
diff --git a/evap/evaluation/tests/test_views.py b/evap/evaluation/tests/test_views.py
--- a/evap/evaluation/tests/test_views.py
+++ b/evap/evaluation/tests/test_views.py
@@ -74,7 +74,7 @@ def test_sends_mail(self):
             user=user,
         )
         self.assertEqual(len(mail.outbox), 1)
-
+        self.assertTrue(mail.outbox[0].reply_to == ["user@institution.example.com"])
 
 class TestChangeLanguageView(WebTest):
     url = '/set_lang'
Repository: e-valuation/EvaP

Issue:
Wrong grade document edit form title
When editing a grade document that holds final grades, the title of the form wrongly shows "Upload midterm grades" (should be "Upload final grades" instead) because the parameter `final_grades` is not correctly set for the template.
This can for example be seen at the course "Operating Systems I (Summer term 2014)" in the test data.


Discussion:

Resolving patch:
diff --git a/evap/grades/views.py b/evap/grades/views.py
--- a/evap/grades/views.py
+++ b/evap/grades/views.py
@@ -165,6 +165,10 @@ def edit_grades(request, semester_id, course_id, grade_document_id):
 
     form = GradeDocumentForm(request.POST or None, request.FILES or None, instance=grade_document)
 
+    final_grades = (
+        grade_document.type == GradeDocument.Type.FINAL_GRADES
+    )  # if parameter is not given, assume midterm grades
+
     if form.is_valid():
         form.save(modifying_user=request.user)
         messages.success(request, _("Successfully updated grades."))
@@ -175,6 +179,7 @@ def edit_grades(request, semester_id, course_id, grade_document_id):
         course=course,
         form=form,
         show_automated_publishing_info=False,
+        final_grades=final_grades,
     )
     return render(request, "grades_upload_form.html", template_data)
 


Test patch:
diff --git a/evap/grades/tests.py b/evap/grades/tests.py
--- a/evap/grades/tests.py
+++ b/evap/grades/tests.py
@@ -6,6 +6,7 @@
 from model_bakery import baker
 
 from evap.evaluation.models import Contribution, Course, Evaluation, Questionnaire, Semester, UserProfile
+from evap.grades.models import GradeDocument
 
 
 class GradeUploadTest(WebTest):
@@ -238,3 +239,32 @@ def test_403_on_archived_semester(self):
             Evaluation, course=baker.make(Course, pk=1, semester=archived_semester), state=Evaluation.State.PREPARED
         )
         self.app.get("/grades/semester/1/course/1", user=self.grade_publisher, status=403)
+
+
+class GradeEditTest(WebTest):
+    def test_grades_headlines(self):
+
+        grade_publisher = baker.make(
+            UserProfile,
+            email="grade_publisher@institution.example.com",
+            groups=[Group.objects.get(name="Grade publisher")],
+        )
+        grade_document = baker.make(GradeDocument)
+
+        url = f"/grades/semester/{grade_document.course.semester.pk}/course/{grade_document.course.pk}/edit/{grade_document.pk}"
+
+        response = self.app.get(
+            url,
+            user=grade_publisher,
+        )
+        self.assertContains(response, "Upload midterm grades")
+        self.assertNotContains(response, "Upload final grades")
+
+        grade_document.type = GradeDocument.Type.FINAL_GRADES
+        grade_document.save()
+        response = self.app.get(
+            url,
+            user=grade_publisher,
+        )
+        self.assertContains(response, "Upload final grades")
+        self.assertNotContains(response, "Upload midterm grades")
Repository: e-valuation/EvaP

Issue:
Make Typescript code Prettier
We should add automated formatting for our typescript files. I think https://prettier.io/ is pretty good, but the choice is open for discussion. The formatting should be done in `manage.py format` and be checked in CI.


Discussion:

Resolving patch:
diff --git a/evap/evaluation/management/commands/format.py b/evap/evaluation/management/commands/format.py
--- a/evap/evaluation/management/commands/format.py
+++ b/evap/evaluation/management/commands/format.py
@@ -11,3 +11,4 @@ class Command(BaseCommand):
     def handle(self, *args, **options):
         subprocess.run(["black", "evap"], check=False)  # nosec
         subprocess.run(["isort", "."], check=False)  # nosec
+        subprocess.run(["npx", "prettier", "--write", "evap/static/ts/src"], check=False)  # nosec


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -69,22 +69,29 @@ jobs:
   formatter:
     runs-on: ubuntu-18.04
 
-    container:
-      image: python:3.7
-
     name: Formatting
 
     steps:
       - name: Check out repository code
         uses: actions/checkout@v2
-      - name: Install dependencies
+      - uses: actions/setup-python@v2
+        with:
+          python-version: 3.7
+      - name: Install Python dependencies
         run: pip install -r requirements-dev.txt
+      - name: Setup Node
+        uses: actions/setup-node@v2
+      - name: Install Node dependencies
+        run: npm ci
       - name: Add localsettings
         run: cp evap/settings_test.py evap/localsettings.py
       - name: Check code formatting
         run: black --check evap
       - name: Check imports formatting
         run: isort . --check --diff
+      - run: ls -laR evap/static/ts
+      - name: Check TypeScript formatting
+        run: npx prettier --list-different --loglevel debug --config evap/static/ts/.prettierrc.json evap/static/ts/src
 
 
   backup-process:
diff --git a/evap/evaluation/tests/test_commands.py b/evap/evaluation/tests/test_commands.py
--- a/evap/evaluation/tests/test_commands.py
+++ b/evap/evaluation/tests/test_commands.py
@@ -352,11 +352,12 @@ class TestFormatCommand(TestCase):
     @patch("subprocess.run")
     def test_formatters_called(self, mock_subprocess_run):
         management.call_command("format")
-        self.assertEqual(len(mock_subprocess_run.mock_calls), 2)
+        self.assertEqual(len(mock_subprocess_run.mock_calls), 3)
         mock_subprocess_run.assert_has_calls(
             [
                 call(["black", "evap"], check=False),
                 call(["isort", "."], check=False),
+                call(["npx", "prettier", "--write", "evap/static/ts/src"], check=False),
             ]
         )