diff --git a/evap/development/fixtures/test_data.json b/evap/development/fixtures/test_data.json index 073ec62c7c..1f075dc742 100644 --- a/evap/development/fixtures/test_data.json +++ b/evap/development/fixtures/test_data.json @@ -20739,6 +20739,7 @@ "text_de": "Aus der Vorlesung habe ich etwas mitnehmen können.", "text_en": "I learned something in the lecture.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -20751,6 +20752,7 @@ "text_de": "Die in der Vorlesung vermittelten Techniken schienen aktuell.", "text_en": "The techniques taught in the lecture seemed to be up to date.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -20763,6 +20765,7 @@ "text_de": "Die Inhalte der Vorlesung bauten sinnvoll aufeinander auf.", "text_en": "The content of the lecture was well structured.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -20775,6 +20778,7 @@ "text_de": "Die Übung trug zu meinem Verständnis bei.", "text_en": "The exercise contributed to my comprehension of the lecture.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -20787,6 +20791,7 @@ "text_de": "Vorlesung und Übung waren gut aufeinander abgestimmt", "text_en": "Lecture and exercise were well synchronized.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -20799,6 +20804,7 @@ "text_de": "Das fachliche Niveau der Übung war angemessen.", "text_en": "The technical level of the exercise was appropriate.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -20811,6 +20817,7 @@ "text_de": "Diese Themenblöcke fand ich besonders interessant:", "text_en": "These topics were most interesting for me:", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -20823,6 +20830,7 @@ "text_de": "Diese Themenblöcke fand ich weniger interessant:", "text_en": "These topics were less interesting for me:", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -20835,6 +20843,7 @@ "text_de": "Es gab ausreichend Lehrmittel (Skripte, Folien, Videos, Bücher...)", "text_en": "The amount of provided learning material was sufficient.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -20847,6 +20856,7 @@ "text_de": "Die bereitgestellten Lehrmittel waren hilfreich.", "text_en": "The provided learning material was useful.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -20859,6 +20869,7 @@ "text_de": "Die Lehrmittel standen rechtzeitig zur Verfügung.", "text_en": "The learning material was provided in time.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -20871,6 +20882,7 @@ "text_de": "Was waren die Stärken und die Schwächen der Lehrmittel?", "text_en": "Which were the strengths and weaknesses of the learning material?", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -20883,6 +20895,7 @@ "text_de": "Die Vorlesung hat mir Spaß/ Freude bereitet.", "text_en": "I enjoyed the lecture.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -20895,6 +20908,7 @@ "text_de": "Ich bin zufrieden mit dem Lernerfolg.", "text_en": "I am satisfied with my learning success.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -20907,6 +20921,7 @@ "text_de": "Die Vorlesung hat mich in die Lage versetzt, das Thema selbständig zu vertiefen.", "text_en": "The lecture provided all information and skills that I need to deepen my knowledge in this topic.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -20919,6 +20934,7 @@ "text_de": "Die Vorlesung ist für mein Studium wichtig.", "text_en": "The lecture is important for my studies.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -20931,6 +20947,7 @@ "text_de": "Ich empfand den Zeitaufwand für die Veranstaltung insgesamt als angemessen.", "text_en": "I think the expenditure of time for the course was appropriate.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -20943,6 +20960,7 @@ "text_de": "Sonstige Kommentare", "text_en": "General remarks", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -20955,6 +20973,7 @@ "text_de": "... verhielt sich gegenüber den Studierenden respektvoll.", "text_en": "... showed respect for the students.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -20967,6 +20986,7 @@ "text_de": "... konnte mir Wissen vermitteln.", "text_en": "... imparted knowledge.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -20979,6 +20999,7 @@ "text_de": "... ging auf Fragen und Anregungen ein.", "text_en": "... adressed questions and proposals.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -20991,6 +21012,7 @@ "text_de": "... gestaltete die Veranstaltung interessant.", "text_en": "... gave the course in an interesting way.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21003,6 +21025,7 @@ "text_de": "... wirkte gut vorbereitet.", "text_en": "... seemed to be well prepared.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21015,6 +21038,7 @@ "text_de": "Wie bewertest du die Übung im Hinblick auf Umfang und Nutzen?", "text_en": "How do you evaluate the exercise concerning amount and usefulness?", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -21027,6 +21051,7 @@ "text_de": "Wie bewertest du die Schwierigkeit der Übung?", "text_en": "How do you evaluate the difficulty of the exercises?", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 6 } }, @@ -21039,6 +21064,7 @@ "text_de": "... verhielt sich gegenüber den Studierenden respektvoll.", "text_en": "... showed respect for the students.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21051,6 +21077,7 @@ "text_de": "... konnte mir Wissen vermitteln.", "text_en": "... imparted knowledge.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21063,6 +21090,7 @@ "text_de": "... stand auch außerhalb der regulären Übungstermine zur Verfügung.", "text_en": "... was available even outside the regular exercise meetings.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21075,6 +21103,7 @@ "text_de": "Das Seminar hat mir Spaß/Freude bereitet.", "text_en": "I enjoyed the seminar.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21087,6 +21116,7 @@ "text_de": "Die Seminarthemen fand ich gut ausgewählt.", "text_en": "The seminar's topics were chosen very well.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21099,6 +21129,7 @@ "text_de": "Die bereitgestellten Materialien waren gute Einstiegspunkte für eigene Recherche.", "text_en": "The provided learning material was a good start for own research.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21111,6 +21142,7 @@ "text_de": "Wie bewertest du den Aufwand des Seminars?", "text_en": "How do you evaluate the expenditure of time for the course?", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 8 } }, @@ -21123,6 +21155,7 @@ "text_de": "Aus dem Seminar habe ich etwas mitnehmen können.", "text_en": "I learned something from the seminar.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21135,6 +21168,7 @@ "text_de": "Ich kann nachvollziehen, wie und nach welchen Kriterien die Bewertung erfolgt.", "text_en": "I can understand how the grading is influenced by specific criteria.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21147,6 +21181,7 @@ "text_de": "Was hat das Seminar besonders ausgezeichnet?", "text_en": "What were the strengths of the seminar?", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -21159,6 +21194,7 @@ "text_de": "... verhielt sich gegenüber den Studierenden respektvoll.", "text_en": "... showed respect for the students.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21171,6 +21207,7 @@ "text_de": "... konnte mir Wissen vermitteln.", "text_en": "... imparted knowledge.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21183,6 +21220,7 @@ "text_de": "... ging auf Fragen und Anregungen ein.", "text_en": "... addressed questions and proposals.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21195,6 +21233,7 @@ "text_de": "... gestaltete das Seminar interessant.", "text_en": "... conducted the seminar in an interesting way.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21207,6 +21246,7 @@ "text_de": "... wirkte gut vorbereitet.", "text_en": "... seemed to be well prepared.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21219,6 +21259,7 @@ "text_de": "... stand auch außerhalb des Seminars zur Verfügung.", "text_en": "... was available even outside the seminar.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21231,6 +21272,7 @@ "text_de": "... hat mich/mein Team aktiv betreut.", "text_en": "... made a good job in actively supporting me/my team.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21243,6 +21285,7 @@ "text_de": "Das Projekt hat mir Spaß/Freude bereitet.", "text_en": "I enjoyed the project.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21255,6 +21298,7 @@ "text_de": "Ich empfand den Zeitaufwand für das Projekt als angemessen.", "text_en": "I think the expenditure of time for the project was appropriate.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21267,6 +21311,7 @@ "text_de": "Aus dem Projekt habe ich etwas mitnehmen können.", "text_en": "I learned something from the project.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21279,6 +21324,7 @@ "text_de": "... verhielt sich gegenüber den Studierenden respektvoll.", "text_en": "... showed respect for the students.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21291,6 +21337,7 @@ "text_de": "... konnte mir Wissen vermitteln.", "text_en": "... imparted knowledge.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21303,6 +21350,7 @@ "text_de": "... ging auf Fragen und Anregungen ein.", "text_en": "... addressed questions and proposals.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21315,6 +21363,7 @@ "text_de": "... hat mich/mein Team aktiv betreut.", "text_en": "... made a good job in actively supervising me/my team.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21327,6 +21376,7 @@ "text_de": "... wirkte gut vorbereitet.", "text_en": "... seemed to be well prepared.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21339,6 +21389,7 @@ "text_de": "Hier kannst du Lob und Kritik an den Verantwortlichen der Lehrveranstaltung loswerden. Bitte sei höflich und konstruktiv!", "text_en": "Please give your feedback to the responsible person here. Be polite and constructive!", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -21351,6 +21402,7 @@ "text_de": "... verhielt sich gegenüber den Studierenden respektvoll.", "text_en": "... showed respect for the students.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21363,6 +21415,7 @@ "text_de": "... konnte mir Wissen vermitteln.", "text_en": "... could equip me with knowledge.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21375,6 +21428,7 @@ "text_de": "... ging auf Fragen und Anregungen ein.", "text_en": "... addressed questions and proposals.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21387,6 +21441,7 @@ "text_de": "... gestaltete die Übung interessant.", "text_en": "... conducted the excercise in an interesting way.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21399,6 +21454,7 @@ "text_de": "... wirkte gut vorbereitet.", "text_en": "... seemed to be well-prepared.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21411,6 +21467,7 @@ "text_de": "Hier kannst du Lob und Kritik zur Person loswerden. Bitte sei höflich und konstruktiv!", "text_en": "Please give your feedback to the person here. Be polite and constructive!", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -21423,6 +21480,7 @@ "text_de": "Hier kannst du Lob und Kritik zu weiteren Mitwirkenden loswerden. Bitte sei höflich und konstruktiv!", "text_en": "Please give your feedback to additional contributors here. Be polite and constructive!", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -21435,6 +21493,7 @@ "text_de": "... ging auf Fragen und Anregungen ein.", "text_en": "... addressed questions and proposals.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21447,6 +21506,7 @@ "text_de": "Ich kann nachvollziehen, wie und nach welchen Kriterien die Bewertung erfolgt.", "text_en": "I can understand how the grading is influenced by specific criteria.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21459,6 +21519,7 @@ "text_de": "... stand auch außerhalb der Veranstaltung zur Verfügung.", "text_en": "... was available even outside the course.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21471,6 +21532,7 @@ "text_de": "... stand auch außerhalb der regulären Termine zur Verfügung.", "text_en": "... was avalable even outside the regular meetings.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21483,6 +21545,7 @@ "text_de": "... stand nur selten zur Verfügung.", "text_en": "... was rarely available.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 12 } }, @@ -21495,6 +21558,7 @@ "text_de": "... wirkte gut vorbereitet.", "text_en": "... seemed to be well prepared.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21507,6 +21571,7 @@ "text_de": "Ich kann nachvollziehen, wie und nach welchen Kriterien die Bewertung erfolgt.", "text_en": "I can understand how the grading is influenced by specific criteria.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21519,6 +21584,7 @@ "text_de": "Durch welche Änderungen könnte man das Seminar noch verbessern?", "text_en": "How could the seminar be further improved?", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -21531,6 +21597,7 @@ "text_de": "Das Projekt hat mir Spaß/Freude bereitet.", "text_en": "I enjoyed the project.", "allows_additional_textanswers": true, + "counts_for_grade": false, "type": 1 } }, @@ -21543,6 +21610,7 @@ "text_de": "Das Projekt passt inhaltlich in das bisherige Studium.", "text_en": "The project's topic fits into my studies.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21555,6 +21623,7 @@ "text_de": "Das Projektthema war interessant.", "text_en": "The project's topic was interesting.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21567,6 +21636,7 @@ "text_de": "Das Projektthema passte zu meinen Interessen und Fähigkeiten.", "text_en": "The project matched my interests and skills.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21579,6 +21649,7 @@ "text_de": "Das Projekt entsprach meiner Vorstellung.", "text_en": "The project met my expectations.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21591,6 +21662,7 @@ "text_de": "Die Aufgabenstellung war klar spezifiziert.", "text_en": "The tasks were clearly specified.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21603,6 +21675,7 @@ "text_de": "Bei einer wiederholten Wahl würde ich das gleiche Projekt wählen.", "text_en": "I would choose the same project again.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21615,6 +21688,7 @@ "text_de": "Was hat dir am Projekt besonders gefallen? Wie hätte man das Projekt besser gestalten können?", "text_en": "What was the best part of the project? What could be improved?", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -21627,6 +21701,7 @@ "text_de": "Wie bewertest du den Zeitaufwand für das Projekt?", "text_en": "How do you evaluate the expenditure of time for the project?", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 8 } }, @@ -21639,6 +21714,7 @@ "text_de": "Es haben sich alle Teilnehmer ausgeglichen beteiligt.", "text_en": "All team members participated equally.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21651,6 +21727,7 @@ "text_de": "Die Zeitvorgaben für die Teil-Aufgaben waren realistisch.", "text_en": "The deadlines for sub-task were realistic.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21663,6 +21740,7 @@ "text_de": "Es stand ausreichend Zeit zur Ausarbeitung von Pressemitteilung, Plakat und Präsentation zur Verfügung.", "text_en": "The time for preparation of the press release, poster and presentation was sufficient.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21675,6 +21753,7 @@ "text_de": "Sonstige Kommentare zu Aufwand und Zeitplanung:", "text_en": "Additional remarks on effort and time management:", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -21687,6 +21766,7 @@ "text_de": "Der Professor zeigte Interesse am Projekt und dessen Ergebnis.", "text_en": "The professor was interested in the project and its results.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21699,6 +21779,7 @@ "text_de": "Der Professor hat sich während des Projektzeitraumes eingebracht.", "text_en": "The professor contributed to the project.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21711,6 +21792,7 @@ "text_de": "Der Projektpartner hat sich während des Projektzeitraumes eingebracht.", "text_en": "The business partner contributed to the project.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21723,6 +21805,7 @@ "text_de": "Die Betreuung durch den Lehrstuhl war zufriedenstellend.", "text_en": "The supervision by the chair was satisfying.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21735,6 +21818,7 @@ "text_de": "Die Betreuung durch den Projektpartner war zufriedenstellend.", "text_en": "The supervision by the business project partner was satisfying.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21747,6 +21831,7 @@ "text_de": "Sonstige Kommentare zur Betreuung und zum Projektpartner:", "text_en": "Additional remarks on supervision and the business partner:", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -21759,6 +21844,7 @@ "text_de": "Mein Bachelorarbeitsthema war interessant.", "text_en": "The topic of my bachelor thesis was interesting.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21771,6 +21857,7 @@ "text_de": "Mein Bachelorarbeitsthema passte zu meinen Vertiefungsgebieten.", "text_en": "The topic of my bachelor's thesis matched my areas of specialization.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21783,6 +21870,7 @@ "text_de": "Es war leicht, aus dem Projektthema Themen für die Bachelorarbeiten abzuleiten.", "text_en": "It was easy to derive the topics of the bachelor's theses out of the project's scope.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21795,6 +21883,7 @@ "text_de": "Das Thema meiner Bachelorarbeit war inhaltlich nahe am Projekt.", "text_en": "The topic of my bachelor's thesis was directly derived from the scope of the project.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21807,6 +21896,7 @@ "text_de": "Es stand ausreichend Zeit zur Ausarbeitung der Bachelorarbeit zur Verfügung.", "text_en": "The time for the preparation of my bachelor's thesis was sufficient.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21819,6 +21909,7 @@ "text_de": "Sonstige Kommentare zur Bachelorarbeit:", "text_en": "Additional remarks on the bachelor's thesis:", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -21831,6 +21922,7 @@ "text_de": "... verhielt sich gegenüber den Studierenden respektvoll.", "text_en": "... showed respect for the students.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21843,6 +21935,7 @@ "text_de": "... konnte mir Wissen vermitteln.", "text_en": "... imparted knowledge.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21855,6 +21948,7 @@ "text_de": "... ging auf Fragen und Anregungen ein.", "text_en": "... adressed questions and proposals.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21867,6 +21961,7 @@ "text_de": "... gestaltete die Veranstaltung interessant.", "text_en": "... gave the course in an interesting way.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21879,6 +21974,7 @@ "text_de": "Das Projekt hat mir Spaß/Freude bereitet.", "text_en": "I enjoyed the project.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21891,6 +21987,7 @@ "text_de": "Das Projekt passt inhaltlich in das bisherige Studium.", "text_en": "The project's topic fits into my studies.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21903,6 +22000,7 @@ "text_de": "Das Projektthema war interessant.", "text_en": "The project's topic was interesting.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21915,6 +22013,7 @@ "text_de": "Das Projektthema passte zu meinen Interessen und Fähigkeiten.", "text_en": "The project matched my interests and skills.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21927,6 +22026,7 @@ "text_de": "Das Projekt entsprach meiner Vorstellung.", "text_en": "The project met my expectations.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21939,6 +22039,7 @@ "text_de": "Die Aufgabenstellung war klar spezifiziert.", "text_en": "The tasks were clearly specified.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21951,6 +22052,7 @@ "text_de": "Bei einer wiederholten Wahl würde ich das gleiche Projekt wählen.", "text_en": "I would choose the same project again.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21963,6 +22065,7 @@ "text_de": "Was hat dir am Projekt besonders gefallen? Wie hätte man das Projekt besser gestalten können?", "text_en": "What was the best part of the project? What could be improved?", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -21975,6 +22078,7 @@ "text_de": "Wie bewertest du den Zeitaufwand?", "text_en": "How do you evaluate the expenditure of time?", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 8 } }, @@ -21987,6 +22091,7 @@ "text_de": "Es haben sich alle Teilnehmer ausgeglichen beteiligt.", "text_en": "All team members participated equally.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -21999,6 +22104,7 @@ "text_de": "Die Zeitvorgaben für die Teil-Aufgaben waren realistisch.", "text_en": "The deadlines for sub-tasks were realistic.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22011,6 +22117,7 @@ "text_de": "Sonstige Kommentare zu Aufwand und Zeitplanung:", "text_en": "Additional remarks on effort and time management:", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -22023,6 +22130,7 @@ "text_de": "Der Professor zeigte Interesse am Projekt und dessen Ergebnis.", "text_en": "The professor was interested in the project and its results.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22035,6 +22143,7 @@ "text_de": "Der Professor hat sich während des Projektzeitraumes eingebracht.", "text_en": "The professor contributed to the project.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22047,6 +22156,7 @@ "text_de": "Die Betreuung durch den Lehrstuhl war zufriedenstellend.", "text_en": "The supervision by the chair was satisfying.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22059,6 +22169,7 @@ "text_de": "Sonstige Kommentare zur Betreuung:", "text_en": "Additional remarks on supervision:", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -22071,6 +22182,7 @@ "text_de": "Mein Masterarbeitsthema war interessant.", "text_en": "The topic of my master's thesis was interesting.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22083,6 +22195,7 @@ "text_de": "Mein Masterarbeitsthema passte zu meinen Vertiefungsgebieten.", "text_en": "The topic of my master thesis matched my areas of specialization.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22095,6 +22208,7 @@ "text_de": "Es stand ausreichend Zeit zur Ausarbeitung der Masterarbeit zur Verfügung.", "text_en": "The time for the preparation of my master's thesis was sufficient.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22107,6 +22221,7 @@ "text_de": "Sonstige Kommentare zur Masterarbeit:", "text_en": "Additional remarks on the master's thesis:", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -22119,6 +22234,7 @@ "text_de": "... konnte Sachverhalte sehr gut erklären.", "text_en": "... did explain issues very well.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22131,6 +22247,7 @@ "text_de": "Was hat das Projekt besonders ausgezeichnet?", "text_en": "What were the strengths of the project?", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -22143,6 +22260,7 @@ "text_de": "Durch welche Änderungen könnte man das Projekt noch verbessern?", "text_en": "How could the project be further improved?", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -22155,6 +22273,7 @@ "text_de": "... wirkte gut vorbereitet.", "text_en": "... seemed to be well prepared.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22167,6 +22286,7 @@ "text_de": "Das Projektseminar hat mir Spaß/Freude bereitet.", "text_en": "I enjoyed the project seminar.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22179,6 +22299,7 @@ "text_de": "Die Seminarthemen fand ich gut ausgewählt.", "text_en": "The seminar's topics were chosen very well.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22191,6 +22312,7 @@ "text_de": "Die bereitgestellten Materialien waren gute Einstiegspunkte für eigene Recherche.", "text_en": "The provided learning material was a good start for own research.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22203,6 +22325,7 @@ "text_de": "Ich empfand den Aufwand für das Projektseminar als angemessen.", "text_en": "I think the expenditure of time for the project seminar was appropriate.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22215,6 +22338,7 @@ "text_de": "Aus dem Projektseminar habe ich etwas mitnehmen können.", "text_en": "I learned something from the project seminar.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22227,6 +22351,7 @@ "text_de": "Ich kann nachvollziehen, wie und nach welchen Kriterien die Bewertung erfolgt.", "text_en": "I can understand how the grading is influenced by specific criteria.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22239,6 +22364,7 @@ "text_de": "Die theoretischen Inhalte des Seminars haben mir bei der Bearbeitung des Projekts geholfen.", "text_en": "The theoretical contents of the seminar helped me working on the project.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22251,6 +22377,7 @@ "text_de": "Was hat das Projektseminar besonders ausgezeichnet?", "text_en": "What were the strengths of the project seminar?", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -22263,6 +22390,7 @@ "text_de": "Durch welche Änderungen könnte man das Projektseminar noch verbessern?", "text_en": "How could the project seminar be further improved?", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -22275,6 +22403,7 @@ "text_de": "Das Seminar hat mir Spaß/Freude bereitet.", "text_en": "I enjoyed the seminar.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22287,6 +22416,7 @@ "text_de": "Das Projektthema fand ich gut ausgewählt.", "text_en": "The project topic was chosen very well.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22299,6 +22429,7 @@ "text_de": "Ich empfand den Aufwand für das Seminar als angemessen.", "text_en": "I think the expenditure of time for the seminar was appropriate.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22311,6 +22442,7 @@ "text_de": "Aus dem Seminar habe ich etwas mitnehmen können.", "text_en": "I learned something from the seminar.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22323,6 +22455,7 @@ "text_de": "Ich kann mir vorstellen, Ansätze und Methoden aus dem Design Thinking auch bei zukünftigen Projekten anzuwenden.", "text_en": "I can imagine using design thinking principles and methods in future projects.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22335,6 +22468,7 @@ "text_de": "Das Seminar ist eine Bereicherung für meinen Studiengang.", "text_en": "The seminar is a good additional contribution to my major subject. ", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22347,6 +22481,7 @@ "text_de": "Was hat das Seminar besonders ausgezeichnet?", "text_en": "What were the strengths of the seminar?", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -22359,6 +22494,7 @@ "text_de": "Durch welche Änderungen könnte man das Seminar noch verbessern?", "text_en": "How could the seminar be further improved?", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -22371,6 +22507,7 @@ "text_de": "Das Studienbegleitende Seminar hat mir Spaß/Freude bereitet.", "text_en": "I enjoyed the accompanying seminar.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22383,6 +22520,7 @@ "text_de": "Die Vorträge fand ich gut ausgewählt.", "text_en": "The topics of the talks were chosen very well.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22395,6 +22533,7 @@ "text_de": "Ich empfand den Aufwand für das Studienbegleitende Seminar als angemessen.", "text_en": "I think the expenditure of time for the accompanying seminar was appropriate.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22407,6 +22546,7 @@ "text_de": "Aus dem Studienbegleitenden Seminar habe ich etwas mitnehmen können.", "text_en": "I learned something from the accompanying seminar.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22419,6 +22559,7 @@ "text_de": "Das Studienbegleitende Seminar hat mir beim Einstieg in das Studium geholfen.", "text_en": "The accompanying seminar helped me to get started with my studies.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22431,6 +22572,7 @@ "text_de": "Was hat das Studienbegleitende Seminar besonders ausgezeichnet?", "text_en": "What were the strengths of the accompanying seminar?", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -22443,6 +22585,7 @@ "text_de": "Durch welche Änderungen könnte man das Studienbegleitende Seminar noch verbessern?", "text_en": "How could the accompanying seminar be further improved?", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -22455,6 +22598,7 @@ "text_de": "... verhielt sich gegenüber den Studierenden respektvoll.", "text_en": "... showed respect for the students.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22467,6 +22611,7 @@ "text_de": "... konnte mir Wissen vermitteln.", "text_en": "... imparted knowledge.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22479,6 +22624,7 @@ "text_de": "... ging auf Fragen und Anregungen ein.", "text_en": "... addressed questions and proposals.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22491,6 +22637,7 @@ "text_de": "... gestaltete das Seminar interessant.", "text_en": "... conducted the seminar in an interesting way.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22503,6 +22650,7 @@ "text_de": "... wirkte gut vorbereitet.", "text_en": "... seemed to be well prepared.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22515,6 +22663,7 @@ "text_de": "... hat den Seminarablauf gut gestaltet.", "text_en": "... created a good structure of the seminar.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22527,6 +22676,7 @@ "text_de": "... hat mich/mein Team aktiv betreut.", "text_en": "... made a good job in actively supporting me/my team.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22539,6 +22689,7 @@ "text_de": "... stand auch außerhalb des Seminars zur Verfügung.", "text_en": "... was available even outside the seminar.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22551,6 +22702,7 @@ "text_de": "... verhielt sich gegenüber den Studierenden respektvoll.", "text_en": "... showed respect for the students.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22563,6 +22715,7 @@ "text_de": "... konnte mir Wissen vermitteln.", "text_en": "... imparted knowledge.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22575,6 +22728,7 @@ "text_de": "... ging auf Fragen und Anregungen ein.", "text_en": "... addressed questions and proposals.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22587,6 +22741,7 @@ "text_de": "... wirkte gut vorbereitet.", "text_en": "... seemed to be well prepared.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22599,6 +22754,7 @@ "text_de": "... hat mich/mein Team aktiv betreut.", "text_en": "... made a good job in actively supporting me/my team.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22611,6 +22767,7 @@ "text_de": "... stand auch außerhalb des Seminars zur Verfügung.", "text_en": "... was available even outside the seminar.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -22623,6 +22780,7 @@ "text_de": "Wie würdest du die Veranstaltung insgesamt bewerten?", "text_en": "How would you grade the course in total?", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 2 } }, @@ -22635,6 +22793,7 @@ "text_de": "Würden Sie eine Fortsetzung des Kurses besuchen?", "text_en": "Would you enroll in a continued version of this course?", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 3 } }, @@ -22647,6 +22806,7 @@ "text_de": "Fanden Sie den Kurs zu schwer?", "text_en": "Was the course too difficult?", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 6 } }, @@ -22659,6 +22819,7 @@ "text_de": "Wie empfandest du die Größe des Seminars?", "text_en": "How do you feel about the size of the seminar?", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 9 } }, @@ -22671,6 +22832,7 @@ "text_de": "Es gab genügend Übungen.", "text_en": "There were sufficient exercises.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 7 } }, @@ -22683,6 +22845,7 @@ "text_de": "... ist in einem angenehmen Tempo vorangeschritten.", "text_en": "... had a comfortable pace.", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 10 } }, @@ -22695,6 +22858,7 @@ "text_de": "Wie fandest du die Klausur?", "text_en": "How did you like the exam?", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 6 } }, @@ -22707,6 +22871,7 @@ "text_de": "Wieso hast du den Kurs abgewählt?", "text_en": "Why did you drop this course?", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -22719,6 +22884,7 @@ "text_de": "Wirst du den Kurs nochmal belegen?", "text_en": "Will you take this course again?", "allows_additional_textanswers": true, + "counts_for_grade": false, "type": 3 } }, diff --git a/evap/evaluation/migrations/0161_question_counts_for_grade.py b/evap/evaluation/migrations/0161_question_counts_for_grade.py new file mode 100644 index 0000000000..791d1804e4 --- /dev/null +++ b/evap/evaluation/migrations/0161_question_counts_for_grade.py @@ -0,0 +1,42 @@ +# Generated by Django 5.2 on 2025-05-12 21:30 + +from django.db import migrations, models +from django.db.models import Q + +TEXT = 0 +HEADING = 5 +DROPOUT_QUESTIONNAIRE = 5 + + +def set_initial_values(apps, _schema_editor): + Question = apps.get_model("evaluation", "Question") + + Question.objects.filter(Q(type__in=[TEXT, HEADING]) | Q(questionnaire__type=DROPOUT_QUESTIONNAIRE)).update( + counts_for_grade=False + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("evaluation", "0160_evaluation_staff_notes"), + ] + + operations = [ + migrations.AddField( + model_name="question", + name="counts_for_grade", + field=models.BooleanField(default=True, verbose_name="counts toward the evaluation's grade"), + ), + migrations.RunPython(set_initial_values, reverse_code=migrations.RunPython.noop), + migrations.AddConstraint( + model_name="question", + constraint=models.CheckConstraint( + condition=models.Q( + models.Q(("type", TEXT), ("type", HEADING), _connector="OR", _negated=True), + ("counts_for_grade", False), + _connector="OR", + ), + name="check_evaluation_textanswer_or_heading_question_does_not_count_for_grade", + ), + ), + ] diff --git a/evap/evaluation/models.py b/evap/evaluation/models.py index 79d7c6a901..0d8010c056 100644 --- a/evap/evaluation/models.py +++ b/evap/evaluation/models.py @@ -1268,6 +1268,7 @@ class Question(models.Model): text_en = models.CharField(max_length=1024, verbose_name=_("question text (english)")) text = translate(en="text_en", de="text_de") allows_additional_textanswers = models.BooleanField(default=True, verbose_name=_("allow additional text answers")) + counts_for_grade = models.BooleanField(default=True, verbose_name=_("counts toward the evaluation's grade")) type = models.PositiveSmallIntegerField(choices=QUESTION_TYPES, verbose_name=_("question type")) @@ -1282,14 +1283,21 @@ class Meta: ~(Q(type=QuestionType.TEXT) | Q(type=QuestionType.HEADING)) | ~Q(allows_additional_textanswers=True) ), name="check_evaluation_textanswer_or_heading_question_has_no_additional_textanswers", - ) + ), + CheckConstraint( + condition=(~(Q(type=QuestionType.TEXT) | Q(type=QuestionType.HEADING)) | Q(counts_for_grade=False)), + name="check_evaluation_textanswer_or_heading_question_does_not_count_for_grade", + ), ] def save(self, *args, **kwargs): if self.type in [QuestionType.TEXT, QuestionType.HEADING]: self.allows_additional_textanswers = False + self.counts_for_grade = False if "update_fields" in kwargs: - kwargs["update_fields"] = {"allows_additional_textanswers"}.union(kwargs["update_fields"]) + kwargs["update_fields"] = {"allows_additional_textanswers", "counts_for_grade"}.union( + kwargs["update_fields"] + ) super().save(*args, **kwargs) diff --git a/evap/evaluation/tests/test_models.py b/evap/evaluation/tests/test_models.py index eb4f82b19d..60a6eef200 100644 --- a/evap/evaluation/tests/test_models.py +++ b/evap/evaluation/tests/test_models.py @@ -5,6 +5,7 @@ from django.core import mail from django.core.cache import caches from django.core.exceptions import ValidationError +from django.db import models from django.test import override_settings from django_fsm import TransitionNotAllowed from model_bakery import baker @@ -1146,3 +1147,58 @@ class QuestionnaireTests(TestCase): def test_locked_contributor_questionnaire(self): questionnaire = baker.prepare(Questionnaire, is_locked=True, type=Questionnaire.Type.CONTRIBUTOR) self.assertRaises(ValidationError, questionnaire.clean) + + +class QuestionTests(TestCase): + def test_save_for_text_and_heading_question_type(self): + questionaire = baker.make(Questionnaire) + # Use prepare() instead of make() to test Question.save() method behavior + question_text = baker.prepare( + Question, + questionnaire=questionaire, + type=QuestionType.TEXT, + allows_additional_textanswers=True, + counts_for_grade=True, + ) + question_heading = baker.prepare( + Question, + questionnaire=questionaire, + type=QuestionType.HEADING, + allows_additional_textanswers=True, + counts_for_grade=True, + ) + question_rating = baker.prepare( + Question, + questionnaire=questionaire, + type=QuestionType.NEGATIVE_LIKERT, + allows_additional_textanswers=True, + counts_for_grade=True, + ) + + question_rating.save() + question_rating.refresh_from_db() + self.assertEqual(question_rating.allows_additional_textanswers, True) + self.assertEqual(question_rating.counts_for_grade, True) + + # Check if setting allows_additional_textanswers and counts_for_grade to False in the save method works + question_rating.type = QuestionType.TEXT + question_rating.save(update_fields=["type"]) + question_rating.refresh_from_db() + self.assertEqual(question_rating.allows_additional_textanswers, False) + self.assertEqual(question_rating.counts_for_grade, False) + + with patch.object(models.Model, "save") as mock_save: + question_text.save(update_fields=["text_de"]) + mock_save.assert_called_once() + args, kwargs = mock_save.call_args + self.assertEqual( + set(kwargs["update_fields"]), {"allows_additional_textanswers", "counts_for_grade", "text_de"} + ) + + mock_save.reset_mock() + question_heading.save(update_fields=["text_de"]) + mock_save.assert_called_once() + args, kwargs = mock_save.call_args + self.assertEqual( + set(kwargs["update_fields"]), {"allows_additional_textanswers", "counts_for_grade", "text_de"} + ) diff --git a/evap/evaluation/tests/tools.py b/evap/evaluation/tests/tools.py index 7ebe0039ca..dd3e3b6ca9 100644 --- a/evap/evaluation/tests/tools.py +++ b/evap/evaluation/tests/tools.py @@ -359,6 +359,9 @@ def setUpClass(cls) -> None: super().setUpClass() cls.selenium.set_window_size(*cls.window_size) + def set_tomselect_value(self, instance: WebElement, value: str) -> None: + self.selenium.execute_script(f"arguments[0].tomselect.setValue('{value}');", instance) + def classes_of_element(element: WebElement) -> list[str]: classes = element.get_attribute("class") diff --git a/evap/results/fixtures/minimal_test_data_results.json b/evap/results/fixtures/minimal_test_data_results.json index 81ff276acf..5e723a3e30 100644 --- a/evap/results/fixtures/minimal_test_data_results.json +++ b/evap/results/fixtures/minimal_test_data_results.json @@ -189,6 +189,7 @@ "text_de": "how?", "text_en": "how?", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -201,6 +202,7 @@ "text_de": "how much?", "text_en": "how much?", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -213,6 +215,7 @@ "text_de": "how?", "text_en": "how?", "allows_additional_textanswers": false, + "counts_for_grade": false, "type": 0 } }, @@ -225,6 +228,7 @@ "text_de": "how much?", "text_en": "how much?", "allows_additional_textanswers": true, + "counts_for_grade": true, "type": 1 } }, @@ -237,6 +241,7 @@ "text_de": "your grade", "text_en": "your grade", "allows_additional_textanswers": false, + "counts_for_grade": true, "type": 2 } }, diff --git a/evap/results/tests/test_exporters.py b/evap/results/tests/test_exporters.py index a33c399657..2be5414eed 100644 --- a/evap/results/tests/test_exporters.py +++ b/evap/results/tests/test_exporters.py @@ -544,6 +544,7 @@ def test_text_answer_export(self): _quantity=len(Questionnaire.Type.values), _bulk_create=True, allows_additional_textanswers=False, + counts_for_grade=False, ) baker.make( diff --git a/evap/results/tests/test_tools.py b/evap/results/tests/test_tools.py index 40608198b2..590c5a7076 100644 --- a/evap/results/tests/test_tools.py +++ b/evap/results/tests/test_tools.py @@ -20,6 +20,8 @@ from evap.results.tools import ( ViewContributorResults, ViewGeneralResults, + average_grade_questions_distribution, + average_non_grade_rating_questions_distribution, cache_results, calculate_average_course_distribution, calculate_average_distribution, @@ -417,24 +419,93 @@ def test_calculate_average_course_distribution(self): self.assertEqual(distribution[3], 0) self.assertEqual(distribution[4], 0) - def test_dropout_questionnaires_are_not_included(self): - general_questionnaire = baker.make(Questionnaire, type=Questionnaire.Type.TOP) - general_question = baker.make(Question, questionnaire=general_questionnaire, type=QuestionType.GRADE) + def test_average_questions_distribution(self): + grade_question = baker.make( + Question, questionnaire=self.questionnaire, type=QuestionType.GRADE, counts_for_grade=True + ) + non_counting_grade_question = baker.make( + Question, questionnaire=self.questionnaire, type=QuestionType.GRADE, counts_for_grade=False + ) + likert_question = baker.make( + Question, questionnaire=self.questionnaire, type=QuestionType.POSITIVE_LIKERT, counts_for_grade=True + ) + non_counting_likert_question = baker.make( + Question, questionnaire=self.questionnaire, type=QuestionType.POSITIVE_LIKERT, counts_for_grade=False + ) - dropout_questionnaire = baker.make(Questionnaire, type=Questionnaire.Type.DROPOUT) - dropout_question = baker.make(Question, questionnaire=dropout_questionnaire, type=QuestionType.GRADE) + counters = [ + *make_rating_answer_counters(non_counting_grade_question, self.contribution1, [0, 0, 0, 0, 1], False), + *make_rating_answer_counters(non_counting_likert_question, self.contribution1, [0, 0, 0, 0, 3], False), + ] + RatingAnswerCounter.objects.bulk_create(counters) - contribution = baker.make( - Contribution, evaluation=self.evaluation, questionnaires=[general_questionnaire, dropout_questionnaire] + cache_results(self.evaluation) + evaluation_results = get_results(self.evaluation) + + question_results = [ + question_result + for contribution_result in evaluation_results.contribution_results + for questionnaire_result in contribution_result.questionnaire_results + for question_result in questionnaire_result.question_results + ] + + self.assertIsNone(average_grade_questions_distribution(question_results)) + self.assertIsNone(average_non_grade_rating_questions_distribution(question_results)) + + counters = [ + *make_rating_answer_counters(grade_question, self.contribution1, [1, 0, 0, 0, 0], False), + *make_rating_answer_counters(likert_question, self.contribution1, [0, 0, 3, 0, 0], False), + ] + RatingAnswerCounter.objects.bulk_create(counters) + + cache_results(self.evaluation) + evaluation_results = get_results(self.evaluation) + + question_results = [ + question_result + for contribution_result in evaluation_results.contribution_results + for questionnaire_result in contribution_result.questionnaire_results + for question_result in questionnaire_result.question_results + ] + + grade_distribution = average_grade_questions_distribution(question_results) + self.assertEqual(grade_distribution, (1, 0, 0, 0, 0)) # Only the counting grade question should be included + self.assertAlmostEqual(distribution_to_grade(grade_distribution), 1.0) + + non_grade_distribution = average_non_grade_rating_questions_distribution(question_results) + self.assertEqual( + non_grade_distribution, (0, 0, 1, 0, 0) + ) # Only the counting likert question should be included + self.assertAlmostEqual(distribution_to_grade(non_grade_distribution), 3.0) + + def test_dropout_questionnaire_excluded_from_distribution(self): + make_rating_answer_counters(self.question_grade, self.general_contribution, [0, 0, 0, 0, 10]) + cache_results(self.evaluation) + + distribution_without_dropout = calculate_average_distribution(self.evaluation) + self.assertIsNotNone(distribution_without_dropout) + + dropout_questionnaire = baker.make(Questionnaire, type=Questionnaire.Type.DROPOUT) + dropout_question = baker.make( + Question, + questionnaire=dropout_questionnaire, + type=QuestionType.GRADE, + counts_for_grade=False, ) + self.evaluation.general_contribution.questionnaires.add(dropout_questionnaire) + make_rating_answer_counters(dropout_question, self.evaluation.general_contribution, [10, 0, 0, 0, 0]) + cache_results(self.evaluation) - make_rating_answer_counters(general_question, contribution, [10, 10, 0, 0, 0]) - make_rating_answer_counters(dropout_question, contribution, [0, 0, 0, 0, 10]) + distribution_with_dropout = calculate_average_distribution(self.evaluation) + self.assertEqual(distribution_without_dropout, distribution_with_dropout) + dropout_question.counts_for_grade = True + dropout_question.save() cache_results(self.evaluation) - calculated_grade = distribution_to_grade(calculate_average_distribution(self.evaluation)) - self.assertAlmostEqual(calculated_grade, 1.5) + # Should raise AssertionError because dropout questionnaire has counts_for_grade=True + with self.assertRaises(AssertionError): + calculate_average_distribution(self.evaluation) class TestTextAnswerVisibilityInfo(TestCase): diff --git a/evap/results/tools.py b/evap/results/tools.py index 78280d7908..b46e1fe5e8 100644 --- a/evap/results/tools.py +++ b/evap/results/tools.py @@ -324,7 +324,7 @@ def average_grade_questions_distribution(results): [ (unipolarized_distribution(result), result.count_sum) for result in results - if result.question.is_grade_question + if result.question.is_grade_question and result.question.counts_for_grade ] ) @@ -334,7 +334,7 @@ def average_non_grade_rating_questions_distribution(results): [ (unipolarized_distribution(result), result.count_sum) for result in results - if result.question.is_non_grade_rating_question + if result.question.is_non_grade_rating_question and result.question.counts_for_grade ] ) @@ -393,7 +393,9 @@ def calculate_average_distribution(evaluation): grouped_results = defaultdict(list) for contribution_result in get_results(evaluation).contribution_results: for questionnaire_result in contribution_result.questionnaire_results: - if not questionnaire_result.questionnaire.is_dropout: # dropout questionnaires are not counted + if questionnaire_result.questionnaire.is_dropout: # dropout questionnaires are not counted + assert not any(result.question.counts_for_grade for result in questionnaire_result.question_results) + if not questionnaire_result.questionnaire.is_dropout: grouped_results[contribution_result.contributor].extend(questionnaire_result.question_results) evaluation_results = grouped_results.pop(None, []) @@ -413,6 +415,8 @@ def calculate_average_distribution(evaluation): ), ] ), + # The weight of this contributors grade is supposed to represent the number of students the + # contributor interacted with, which we derive from the max answer count, independently of counts_for_grades. max( (result.count_sum for result in contributor_results if result.question.is_rating_question), default=0, diff --git a/evap/staff/forms.py b/evap/staff/forms.py index 8e3183d2ca..4572a2a758 100644 --- a/evap/staff/forms.py +++ b/evap/staff/forms.py @@ -907,22 +907,29 @@ def save(self, commit=True): class QuestionForm(forms.ModelForm): class Meta: model = Question - fields = ("order", "questionnaire", "text_de", "text_en", "type", "allows_additional_textanswers") + fields = ( + "order", + "questionnaire", + "text_de", + "text_en", + "type", + "allows_additional_textanswers", + "counts_for_grade", + ) widgets = { "text_de": forms.Textarea(attrs={"rows": 2}), "text_en": forms.Textarea(attrs={"rows": 2}), "order": forms.HiddenInput(), } - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - if self.instance.pk and self.instance.type in [QuestionType.TEXT, QuestionType.HEADING]: - self.fields["allows_additional_textanswers"].disabled = True - def clean(self): super().clean() + questionnaire = self.cleaned_data.get("questionnaire") + if questionnaire and questionnaire.is_dropout: + self.cleaned_data["counts_for_grade"] = False if self.cleaned_data.get("type") in [QuestionType.TEXT, QuestionType.HEADING]: self.cleaned_data["allows_additional_textanswers"] = False + self.cleaned_data["counts_for_grade"] = False return self.cleaned_data diff --git a/evap/staff/templates/staff_questionnaire_form.html b/evap/staff/templates/staff_questionnaire_form.html index 2201d91771..a03b7fb8d5 100644 --- a/evap/staff/templates/staff_questionnaire_form.html +++ b/evap/staff/templates/staff_questionnaire_form.html @@ -57,10 +57,14 @@
{% translate 'Questions' %}
{% include 'bootstrap_form_field_widget.html' with field=form_element.type %}
+ type="checkbox" data-keep="true" {% if form_element.allows_additional_textanswers.value %} checked{% endif %}{% if form_element.allows_additional_textanswers.field.disabled %} disabled{% endif %}/>
+
+ + +
{% include 'bootstrap_form_field_widget.html' with field=form_element.DELETE %} @@ -83,6 +87,10 @@
{% translate 'Questions' %}
{% if editable %} {% endif %} {% endblock %} diff --git a/evap/staff/tests/test_forms.py b/evap/staff/tests/test_forms.py index 9846e7cadc..c9a58c604c 100644 --- a/evap/staff/tests/test_forms.py +++ b/evap/staff/tests/test_forms.py @@ -36,6 +36,7 @@ EvaluationCopyForm, EvaluationEmailForm, EvaluationForm, + QuestionForm, QuestionnaireForm, UserForm, ) @@ -1149,3 +1150,53 @@ def test_save_makes_a_copy(self): copied_evaluation = form.save() self.assertNotEqual(copied_evaluation, self.evaluation) self.assertEqual(Evaluation.objects.count(), 2) + + +class QuestionFormTests(TestCase): + def test_allows_additional_textanswers_persistence_after_type_change(self): + """ + Test that allows_additional_textanswers can be changed and persisted correctly + even after changing the question type from TEXT/HEADING to a Likert type. + Regression test for #2539. + """ + question = baker.make( + Question, + questionnaire__type=Questionnaire.Type.TOP, + type=QuestionType.TEXT, + allows_additional_textanswers=False, + ) + + # First save: set allows_additional_textanswers to False + form_data = get_form_data_from_instance(QuestionForm, question) + self.assertFalse(form_data["allows_additional_textanswers"]) + + form = QuestionForm(form_data, instance=question) + self.assertTrue(form.is_valid()) + form.save() + + question.refresh_from_db() + self.assertFalse(question.allows_additional_textanswers) + + form_data = get_form_data_from_instance(QuestionForm, question) + form_data["type"] = QuestionType.POSITIVE_LIKERT + form_data["allows_additional_textanswers"] = True + + form = QuestionForm(form_data, instance=question) + self.assertTrue(form.is_valid()) + form.save() + + question.refresh_from_db() + self.assertEqual(question.type, QuestionType.POSITIVE_LIKERT) + self.assertTrue(question.allows_additional_textanswers) + + def test_clean_for_dropout_questionnaire(self): + dropout_questionnaire = baker.make(Questionnaire, type=Questionnaire.Type.DROPOUT) + question = baker.make(Question, questionnaire=dropout_questionnaire, type=QuestionType.POSITIVE_LIKERT) + + form_data = get_form_data_from_instance(QuestionForm, question) + form_data["counts_for_grade"] = True + + form = QuestionForm(form_data, instance=question) + # The clean method should override counts_for_grade to False + self.assertTrue(form.is_valid()) + self.assertFalse(form.cleaned_data["counts_for_grade"]) diff --git a/evap/staff/tests/test_live.py b/evap/staff/tests/test_live.py index 35ac73d67f..2df4d06804 100644 --- a/evap/staff/tests/test_live.py +++ b/evap/staff/tests/test_live.py @@ -18,6 +18,7 @@ Program, Question, Questionnaire, + QuestionType, Semester, TextAnswer, UserProfile, @@ -178,6 +179,85 @@ def test_collapse_without_editor_approved(self) -> None: self.assertEqual(counter.text, "0") +class QuestionnaireFormLiveTest(LiveServerTest): + def test_question_type_disabling_logic(self): + def assert_type_allows(row, type_select, question_type, additional_textanswers, counts_for_grade): + self.set_tomselect_value(type_select, str(question_type)) + self.assertNotEqual( + additional_textanswers, + bool( + row.find_element(By.CSS_SELECTOR, "input[id$='-allows_additional_textanswers']").get_attribute( + "disabled" + ) + ), + ) + self.assertNotEqual( + counts_for_grade, + bool(row.find_element(By.CSS_SELECTOR, "input[id$='-counts_for_grade']").get_attribute("disabled")), + ) + + questionnaire = baker.make(Questionnaire, type=Questionnaire.Type.TOP) + baker.make(Question, questionnaire=questionnaire, type=QuestionType.POSITIVE_LIKERT) + + with self.enter_staff_mode(): + self.selenium.get(self.reverse("staff:questionnaire_edit", args=[questionnaire.pk])) + + # Part 1: Edit Existing Question + row = self.wait.until(visibility_of_element_located((By.CSS_SELECTOR, "#question_table tbody tr"))) + type_select = row.find_element(By.CSS_SELECTOR, "select[id$='-type']") + + assert_type_allows(row, type_select, QuestionType.TEXT, additional_textanswers=False, counts_for_grade=False) + assert_type_allows( + row, type_select, QuestionType.POSITIVE_LIKERT, additional_textanswers=True, counts_for_grade=True + ) + assert_type_allows(row, type_select, QuestionType.HEADING, additional_textanswers=False, counts_for_grade=False) + + # Part 2: Add New Question + self.selenium.find_element(By.CLASS_NAME, "add-row").click() + + # Wait until there are at least 3 rows (2 existing (since there is the default new row) + 1 new) + self.wait.until(lambda driver: len(driver.find_elements(By.CSS_SELECTOR, "#question_table tbody tr")) >= 3) + + new_row = self.selenium.find_elements(By.CSS_SELECTOR, "#question_table tbody tr")[ + -2 + ] # the last row is the add row button + new_type_select = new_row.find_element(By.CSS_SELECTOR, "select[id$='-type']") + + assert_type_allows( + new_row, new_type_select, QuestionType.TEXT, additional_textanswers=False, counts_for_grade=False + ) + assert_type_allows( + new_row, new_type_select, QuestionType.POSITIVE_LIKERT, additional_textanswers=True, counts_for_grade=True + ) + assert_type_allows( + new_row, new_type_select, QuestionType.HEADING, additional_textanswers=False, counts_for_grade=False + ) + + def test_questionnaire_type_disabling_logic(self): + questionnaire = baker.make(Questionnaire, type=Questionnaire.Type.TOP) + baker.make(Question, questionnaire=questionnaire, type=QuestionType.POSITIVE_LIKERT) + + with self.enter_staff_mode(): + self.selenium.get(self.reverse("staff:questionnaire_edit", args=[questionnaire.pk])) + + row = self.wait.until(visibility_of_element_located((By.CSS_SELECTOR, "#question_table tbody tr"))) + questionnaire_type_select = self.selenium.find_element(By.ID, "id_type") + + # Change to Dropout + self.set_tomselect_value(questionnaire_type_select, str(Questionnaire.Type.DROPOUT)) + self.assertTrue(row.find_element(By.CSS_SELECTOR, "input[id$='-counts_for_grade']").get_attribute("disabled")) + self.assertFalse( + row.find_element(By.CSS_SELECTOR, "input[id$='-allows_additional_textanswers']").get_attribute("disabled") + ) + + # Change back to Top + self.set_tomselect_value(questionnaire_type_select, str(Questionnaire.Type.TOP)) + self.assertFalse(row.find_element(By.CSS_SELECTOR, "input[id$='-counts_for_grade']").get_attribute("disabled")) + self.assertFalse( + row.find_element(By.CSS_SELECTOR, "input[id$='-allows_additional_textanswers']").get_attribute("disabled") + ) + + class TextAnswerEditLiveTest(LiveServerTest): def test_edit_textanswer_redirect(self): """Regression test for #1696""" diff --git a/evap/staff/tests/test_views.py b/evap/staff/tests/test_views.py index 0902f69a18..8e1738743b 100644 --- a/evap/staff/tests/test_views.py +++ b/evap/staff/tests/test_views.py @@ -3010,6 +3010,7 @@ def test_num_queries_is_constant(self): questionnaire=iter(questionnaires), type=QuestionType.TEXT, allows_additional_textanswers=False, + counts_for_grade=False, **kwargs, ) baker.make(TextAnswer, question=iter(questions), contribution=iter(contributions), **kwargs) @@ -3306,6 +3307,12 @@ def test_allowed_type_changes_on_used_questionnaire(self): "Dropout questionnaires should not be changeable to different types", ) + def test_used_questionnaire_disables_custom_checkboxes(self): + page = self.app.get(self.url, user=self.manager) + form = page.forms["questionnaire-form"] + self.assertIn("disabled", form["questions-0-allows_additional_textanswers"].attrs) + self.assertIn("disabled", form["questions-0-counts_for_grade"].attrs) + class TestQuestionnaireViewView(WebTestStaffModeWith200Check): @classmethod @@ -3323,6 +3330,7 @@ def setUpTestData(cls): _quantity=3, _bulk_create=True, allows_additional_textanswers=False, + counts_for_grade=iter([False, True, True]), ) def test_preview_change_language(self): diff --git a/evap/static/ts/src/staff-questionnaire-form.ts b/evap/static/ts/src/staff-questionnaire-form.ts new file mode 100644 index 0000000000..30dc41c187 --- /dev/null +++ b/evap/static/ts/src/staff-questionnaire-form.ts @@ -0,0 +1,111 @@ +import { assertDefined, saneParseInt, selectOrError } from "./utils.js"; + +const QUESTION_TYPE_TEXT = 0; +const QUESTION_TYPE_HEADING = 5; +const QUESTIONNAIRE_TYPE_DROPOUT = 5; + +export class StaffQuestionnaireForm { + private readonly questionTable: HTMLTableElement; + private readonly questionnaireTypeSelect: HTMLSelectElement; + + constructor(questionTable: HTMLTableElement) { + this.questionTable = questionTable; + this.questionnaireTypeSelect = selectOrError("#id_type"); + + this.questionTable.addEventListener("change", this.handleQuestionTypeChange); + this.questionnaireTypeSelect.addEventListener("change", this.handleQuestionnaireTypeChange); + + this.initialize(); + } + private disableAndUncheck = (checkbox: HTMLInputElement) => { + checkbox.checked = false; + checkbox.disabled = true; + }; + + private enableAndInit = (checkbox: HTMLInputElement, initialValue: boolean) => { + // do not override current input user selection, if there is no need to + if (checkbox.disabled) { + checkbox.checked = initialValue; + } + checkbox.disabled = false; + }; + + private disableAndUncheckAll = (checkboxes: NodeListOf) => { + checkboxes.forEach(checkbox => { + this.disableAndUncheck(checkbox as HTMLInputElement); + }); + }; + + private enableAndInitAll = (checkboxes: NodeListOf, initialValue: boolean) => { + checkboxes.forEach(checkbox => { + this.enableAndInit(checkbox as HTMLInputElement, initialValue); + }); + }; + + private handleQuestionTypeChange = (e: Event) => { + const target = e.target as HTMLElement; + const questionTypeCell = target.closest("td.question-type"); + if (questionTypeCell && target.matches("select")) { + const questionTypeSelect = target as HTMLSelectElement; + const questionType = saneParseInt(questionTypeSelect.value); + + const checkboxes = questionTypeCell.querySelectorAll("input[type=checkbox]"); + + if (questionType === QUESTION_TYPE_TEXT || questionType === QUESTION_TYPE_HEADING) { + this.disableAndUncheckAll(checkboxes); + return; + } + + const questionnaireType = saneParseInt(this.questionnaireTypeSelect.value); + if (questionnaireType === QUESTIONNAIRE_TYPE_DROPOUT) { + checkboxes.forEach(checkbox => { + const checkboxElement = checkbox as HTMLInputElement; + if (checkboxElement.classList.contains("counts-for-grade-checkbox")) { + this.disableAndUncheck(checkboxElement); + } else { + this.enableAndInit(checkboxElement, true); + } + }); + } else { + this.enableAndInitAll(checkboxes, true); + } + } + }; + + private handleQuestionnaireTypeChange = () => { + const selectedType = saneParseInt(this.questionnaireTypeSelect.value); + const countsForGradeCheckboxes = document.querySelectorAll(".counts-for-grade-checkbox"); + + countsForGradeCheckboxes.forEach(checkbox => { + const checkboxElement = checkbox as HTMLInputElement; + const questionTypeCell = checkboxElement.closest("td.question-type"); + assertDefined(questionTypeCell); + + const questionTypeSelect = selectOrError("select", questionTypeCell); + + if (questionTypeSelect.value === "") { + return; + } + + if (selectedType === QUESTIONNAIRE_TYPE_DROPOUT) { + this.disableAndUncheck(checkboxElement); + } else { + const questionType = saneParseInt(questionTypeSelect.value); + if (questionType === QUESTION_TYPE_TEXT || questionType === QUESTION_TYPE_HEADING) { + this.disableAndUncheck(checkboxElement); + } else { + this.enableAndInit(checkboxElement, true); + } + } + }); + }; + + private initialize = () => { + // Initialize the state of all checkboxes based on current question types and questionnaire type + const questionTypeSelects = this.questionTable.querySelectorAll("td.question-type select"); + + questionTypeSelects.forEach(select => { + select.dispatchEvent(new Event("change", { bubbles: true })); + }); + }; +} diff --git a/evap/student/forms.py b/evap/student/forms.py index 44b51e9eda..996f49077a 100644 --- a/evap/student/forms.py +++ b/evap/student/forms.py @@ -29,12 +29,14 @@ def from_question(cls, question: Question): class RatingAnswerField(forms.TypedChoiceField): - def __init__(self, widget_choices, *args, allows_textanswer=False, **kwargs): + def __init__(self, widget_choices, *args, allows_textanswer, counts_for_grade, **kwargs): self.allows_textanswer = allows_textanswer + self.counts_for_grade = counts_for_grade kwargs["coerce"] = int kwargs["widget"] = forms.RadioSelect( attrs={ "allows_textanswer": self.allows_textanswer, + "counts_for_grade": self.counts_for_grade, "choices": widget_choices, } ) @@ -47,6 +49,7 @@ def from_question(cls, question: Question): choices=zip(CHOICES[question.type].values, CHOICES[question.type].names, strict=True), label=question.text, allows_textanswer=question.allows_additional_textanswers, + counts_for_grade=question.counts_for_grade, ) diff --git a/evap/student/templates/student_vote_questionnaire_group.html b/evap/student/templates/student_vote_questionnaire_group.html index 348194cad9..1d2250ede1 100644 --- a/evap/student/templates/student_vote_questionnaire_group.html +++ b/evap/student/templates/student_vote_questionnaire_group.html @@ -35,6 +35,9 @@

+ {% if not field.field.widget.attrs.counts_for_grade %} + + {% endif %} {{ field.label }} {% if allows_textanswer %}