Skip to content

Commit

Permalink
Positive and negative agreement (likert) questions (#1969)
Browse files Browse the repository at this point in the history
* Fixes #1963

* Fixed old tests

* Introduced new test

* Applied black

* questions/likert_questions [Refactor]: Solved Niklas comments

* questions/negative_likert [Refactor]: Added tests

* applied black

* add warning for inverted answer scales

* Fixed janno2

* Rebased

* Finished+

* Isort

* Rebased

* BMBF

* PR feedback

* update test_data to include a question with a new type

---------

Co-authored-by: FSadrieh <[email protected]>
Co-authored-by: Johannes Wolf <[email protected]>
Co-authored-by: Richard Ebeling <[email protected]>
Co-authored-by: Niklas Mohrin <[email protected]>
  • Loading branch information
5 people authored Sep 23, 2023
1 parent ad96350 commit e3c56c6
Show file tree
Hide file tree
Showing 12 changed files with 1,117 additions and 53 deletions.
958 changes: 954 additions & 4 deletions evap/development/fixtures/test_data.json

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions evap/evaluation/migrations/0140_alter_question_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Generated by Django 4.2.3 on 2023-07-17 21:24

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("evaluation", "0139_userprofile_startpage"),
]

operations = [
migrations.AlterField(
model_name="question",
name="type",
field=models.PositiveSmallIntegerField(
choices=[
("Text", ((0, "Text question"),)),
("Unipolar Likert", ((1, "Positive agreement question"), (12, "Negative agreement question"))),
("Grade", ((2, "Grade question"),)),
(
"Bipolar Likert",
(
(6, "Easy-difficult question"),
(7, "Few-many question"),
(8, "Little-much question"),
(9, "Small-large question"),
(10, "Slow-fast question"),
(11, "Short-long question"),
),
),
("Yes-no", ((3, "Positive yes-no question"), (4, "Negative yes-no question"))),
("Layout", ((5, "Heading"),)),
],
verbose_name="question type",
),
),
]
43 changes: 37 additions & 6 deletions evap/evaluation/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1088,7 +1088,8 @@ def remove_answers_to_questionnaires(self, questionnaires):

class QuestionType:
TEXT = 0
LIKERT = 1
POSITIVE_LIKERT = 1
NEGATIVE_LIKERT = 12
GRADE = 2
EASY_DIFFICULT = 6
FEW_MANY = 7
Expand All @@ -1106,7 +1107,13 @@ class Question(models.Model):

QUESTION_TYPES = (
(_("Text"), ((QuestionType.TEXT, _("Text question")),)),
(_("Unipolar Likert"), ((QuestionType.LIKERT, _("Agreement question")),)),
(
_("Unipolar Likert"),
(
(QuestionType.POSITIVE_LIKERT, _("Positive agreement question")),
(QuestionType.NEGATIVE_LIKERT, _("Negative agreement question")),
),
),
(_("Grade"), ((QuestionType.GRADE, _("Grade question")),)),
(
_("Bipolar Likert"),
Expand Down Expand Up @@ -1168,8 +1175,12 @@ def answer_class(self):
raise AssertionError(f"Unknown answer type: {self.type!r}")

@property
def is_likert_question(self):
return self.type == QuestionType.LIKERT
def is_positive_likert_question(self):
return self.type == QuestionType.POSITIVE_LIKERT

@property
def is_negative_likert_question(self):
return self.type == QuestionType.NEGATIVE_LIKERT

@property
def is_bipolar_likert_question(self):
Expand Down Expand Up @@ -1207,7 +1218,8 @@ def is_rating_question(self):
return (
self.is_grade_question
or self.is_bipolar_likert_question
or self.is_likert_question
or self.is_positive_likert_question
or self.is_negative_likert_question
or self.is_yes_no_question
)

Expand All @@ -1231,6 +1243,7 @@ class Choices(NamedTuple):
colors: tuple[str]
grades: tuple[Number]
names: list[StrOrPromise]
is_inverted: bool


class BipolarChoices(NamedTuple):
Expand All @@ -1241,6 +1254,7 @@ class BipolarChoices(NamedTuple):
names: list[StrOrPromise]
plus_name: StrOrPromise
minus_name: StrOrPromise
is_inverted: bool


NO_ANSWER = 6
Expand All @@ -1256,6 +1270,7 @@ class BipolarChoices(NamedTuple):
"values": (-3, -2, -1, 0, 1, 2, 3, NO_ANSWER),
"colors": ("red", "orange", "lime", "green", "lime", "orange", "red", "gray"),
"grades": (5, 11 / 3, 7 / 3, 1, 7 / 3, 11 / 3, 5),
"is_inverted": False,
}

BASE_YES_NO_CHOICES = {
Expand All @@ -1266,7 +1281,7 @@ class BipolarChoices(NamedTuple):
}

CHOICES: dict[int, Choices | BipolarChoices] = {
QuestionType.LIKERT: Choices(
QuestionType.POSITIVE_LIKERT: Choices(
names=[
_("Strongly\nagree"),
_("Agree"),
Expand All @@ -1275,6 +1290,19 @@ class BipolarChoices(NamedTuple):
_("Strongly\ndisagree"),
_("No answer"),
],
is_inverted=False,
**BASE_UNIPOLAR_CHOICES, # type: ignore
),
QuestionType.NEGATIVE_LIKERT: Choices(
names=[
_("Strongly\ndisagree"),
_("Disagree"),
_("Neutral"),
_("Agree"),
_("Strongly\nagree"),
_("No answer"),
],
is_inverted=True,
**BASE_UNIPOLAR_CHOICES, # type: ignore
),
QuestionType.GRADE: Choices(
Expand All @@ -1286,6 +1314,7 @@ class BipolarChoices(NamedTuple):
"5",
_("No answer"),
],
is_inverted=False,
**BASE_UNIPOLAR_CHOICES, # type: ignore
),
QuestionType.EASY_DIFFICULT: BipolarChoices(
Expand Down Expand Up @@ -1384,6 +1413,7 @@ class BipolarChoices(NamedTuple):
_("No"),
_("No answer"),
],
is_inverted=False,
**BASE_YES_NO_CHOICES, # type: ignore
),
QuestionType.NEGATIVE_YES_NO: Choices(
Expand All @@ -1392,6 +1422,7 @@ class BipolarChoices(NamedTuple):
_("Yes"),
_("No answer"),
],
is_inverted=True,
**BASE_YES_NO_CHOICES, # type: ignore
),
}
Expand Down
2 changes: 1 addition & 1 deletion evap/evaluation/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ def test_second_vote_sets_can_publish_text_results_to_true(self):
)
evaluation.save()
top_general_questionnaire = baker.make(Questionnaire, type=Questionnaire.Type.TOP)
baker.make(Question, questionnaire=top_general_questionnaire, type=QuestionType.LIKERT)
baker.make(Question, questionnaire=top_general_questionnaire, type=QuestionType.POSITIVE_LIKERT)
evaluation.general_contribution.questionnaires.set([top_general_questionnaire])

self.assertFalse(evaluation.can_publish_text_results)
Expand Down
30 changes: 29 additions & 1 deletion evap/evaluation/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from django_webtest import WebTest
from model_bakery import baker

from evap.evaluation.models import UserProfile
from evap.evaluation.models import Evaluation, Question, QuestionType, UserProfile
from evap.evaluation.tests.tools import WebTestWith200Check, create_evaluation_with_responsible_and_editor


Expand Down Expand Up @@ -201,3 +201,31 @@ def test_edit_display_name(self):

page = self.app.get(self.url, user=self.responsible)
self.assertContains(page, "testdisplayname")


class TestNegativeLikertQuestions(WebTest):
@classmethod
def setUpTestData(cls):
cls.voting_user = baker.make(UserProfile, email="[email protected]")

cls.evaluation = baker.make(
Evaluation,
participants=[cls.voting_user],
state=Evaluation.State.IN_EVALUATION,
)

cls.question = baker.make(
Question,
type=QuestionType.NEGATIVE_LIKERT,
text_en="Negative Likert Question",
text_de="Negative Likert Frage",
)

cls.evaluation.general_contribution.questionnaires.add(cls.question.questionnaire)

cls.url = reverse("student:vote", args=[cls.evaluation.pk])

def test_answer_ordering(self):
page = self.app.get(self.url, user=self.voting_user, status=200).body.decode()
self.assertLess(page.index("Strongly<br>disagree"), page.index("Strongly<br>agree"))
self.assertIn("The answer scale is inverted for this question", page)
28 changes: 15 additions & 13 deletions evap/results/tests/test_exporters.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ def test_questionnaire_ordering(self):
questionnaire_3 = baker.make(Questionnaire, order=1, type=Questionnaire.Type.BOTTOM)
questionnaire_4 = baker.make(Questionnaire, order=4, type=Questionnaire.Type.BOTTOM)

question_1 = baker.make(Question, type=QuestionType.LIKERT, questionnaire=questionnaire_1)
question_2 = baker.make(Question, type=QuestionType.LIKERT, questionnaire=questionnaire_2)
question_3 = baker.make(Question, type=QuestionType.LIKERT, questionnaire=questionnaire_3)
question_4 = baker.make(Question, type=QuestionType.LIKERT, questionnaire=questionnaire_4)
question_1 = baker.make(Question, type=QuestionType.POSITIVE_LIKERT, questionnaire=questionnaire_1)
question_2 = baker.make(Question, type=QuestionType.POSITIVE_LIKERT, questionnaire=questionnaire_2)
question_3 = baker.make(Question, type=QuestionType.POSITIVE_LIKERT, questionnaire=questionnaire_3)
question_4 = baker.make(Question, type=QuestionType.POSITIVE_LIKERT, questionnaire=questionnaire_4)

evaluation.general_contribution.questionnaires.set(
[questionnaire_1, questionnaire_2, questionnaire_3, questionnaire_4]
Expand Down Expand Up @@ -110,7 +110,7 @@ def test_heading_question_filtering(self):
questionnaire = baker.make(Questionnaire)
baker.make(Question, type=QuestionType.HEADING, questionnaire=questionnaire, order=0)
heading_question = baker.make(Question, type=QuestionType.HEADING, questionnaire=questionnaire, order=1)
likert_question = baker.make(Question, type=QuestionType.LIKERT, questionnaire=questionnaire, order=2)
likert_question = baker.make(Question, type=QuestionType.POSITIVE_LIKERT, questionnaire=questionnaire, order=2)
baker.make(Question, type=QuestionType.HEADING, questionnaire=questionnaire, order=3)

contribution = baker.make(
Expand Down Expand Up @@ -202,7 +202,7 @@ def test_course_type_ordering(self):
cache_results(evaluation_2)

questionnaire = baker.make(Questionnaire)
question = baker.make(Question, type=QuestionType.LIKERT, questionnaire=questionnaire)
question = baker.make(Question, type=QuestionType.POSITIVE_LIKERT, questionnaire=questionnaire)

evaluation_1.general_contribution.questionnaires.set([questionnaire])
make_rating_answer_counters(question, evaluation_1.general_contribution)
Expand Down Expand Up @@ -359,9 +359,9 @@ def test_exclude_used_but_unanswered_questionnaires(self):
course__degrees=[degree],
)
used_questionnaire = baker.make(Questionnaire)
used_question = baker.make(Question, type=QuestionType.LIKERT, questionnaire=used_questionnaire)
used_question = baker.make(Question, type=QuestionType.POSITIVE_LIKERT, questionnaire=used_questionnaire)
unused_questionnaire = baker.make(Questionnaire)
unused_question = baker.make(Question, type=QuestionType.LIKERT, questionnaire=unused_questionnaire)
unused_question = baker.make(Question, type=QuestionType.POSITIVE_LIKERT, questionnaire=unused_questionnaire)

evaluation.general_contribution.questionnaires.set([used_questionnaire, unused_questionnaire])
make_rating_answer_counters(used_question, evaluation.general_contribution)
Expand Down Expand Up @@ -413,8 +413,8 @@ def test_correct_grades_and_bottom_numbers(self):
)
questionnaire1 = baker.make(Questionnaire, order=1)
questionnaire2 = baker.make(Questionnaire, order=2)
question1 = baker.make(Question, type=QuestionType.LIKERT, questionnaire=questionnaire1)
question2 = baker.make(Question, type=QuestionType.LIKERT, questionnaire=questionnaire2)
question1 = baker.make(Question, type=QuestionType.POSITIVE_LIKERT, questionnaire=questionnaire1)
question2 = baker.make(Question, type=QuestionType.POSITIVE_LIKERT, questionnaire=questionnaire2)

make_rating_answer_counters(question1, evaluation.general_contribution, [1, 0, 1, 0, 0])
make_rating_answer_counters(question2, evaluation.general_contribution, [0, 1, 0, 1, 0])
Expand Down Expand Up @@ -448,7 +448,7 @@ def test_course_grade(self):
expected_average = 2.0

questionnaire = baker.make(Questionnaire)
question = baker.make(Question, type=QuestionType.LIKERT, questionnaire=questionnaire)
question = baker.make(Question, type=QuestionType.POSITIVE_LIKERT, questionnaire=questionnaire)
for grades, e in zip(grades_per_eval, evaluations):
make_rating_answer_counters(question, e.general_contribution, grades)
e.general_contribution.questionnaires.set([questionnaire])
Expand Down Expand Up @@ -504,8 +504,10 @@ def test_contributor_result_export(self):

general_questionnaire = baker.make(Questionnaire, type=Questionnaire.Type.TOP)
contributor_questionnaire = baker.make(Questionnaire, type=Questionnaire.Type.CONTRIBUTOR)
general_question = baker.make(Question, type=QuestionType.LIKERT, questionnaire=general_questionnaire)
contributor_question = baker.make(Question, type=QuestionType.LIKERT, questionnaire=contributor_questionnaire)
general_question = baker.make(Question, type=QuestionType.POSITIVE_LIKERT, questionnaire=general_questionnaire)
contributor_question = baker.make(
Question, type=QuestionType.POSITIVE_LIKERT, questionnaire=contributor_questionnaire
)

evaluation_1.general_contribution.questionnaires.set([general_questionnaire])
make_rating_answer_counters(general_question, evaluation_1.general_contribution, [2, 0, 0, 0, 0])
Expand Down
20 changes: 14 additions & 6 deletions evap/results/tests/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,11 @@ def setUpTestData(cls):
)
cls.questionnaire = baker.make(Questionnaire)
cls.question_grade = baker.make(Question, questionnaire=cls.questionnaire, type=QuestionType.GRADE)
cls.question_likert = baker.make(Question, questionnaire=cls.questionnaire, type=QuestionType.LIKERT)
cls.question_likert_2 = baker.make(Question, questionnaire=cls.questionnaire, type=QuestionType.LIKERT)
cls.question_likert = baker.make(Question, questionnaire=cls.questionnaire, type=QuestionType.POSITIVE_LIKERT)
cls.question_likert_2 = baker.make(Question, questionnaire=cls.questionnaire, type=QuestionType.POSITIVE_LIKERT)
cls.question_negative_likert = baker.make(
Question, questionnaire=cls.questionnaire, type=QuestionType.NEGATIVE_LIKERT
)
cls.question_bipolar = baker.make(Question, questionnaire=cls.questionnaire, type=QuestionType.FEW_MANY)
cls.question_bipolar_2 = baker.make(Question, questionnaire=cls.questionnaire, type=QuestionType.LITTLE_MUCH)
cls.general_contribution = cls.evaluation.general_contribution
Expand Down Expand Up @@ -214,6 +217,9 @@ def test_average_grade(self):
*make_rating_answer_counters(self.question_likert, self.contribution1, [0, 0, 4, 0, 0], False),
*make_rating_answer_counters(self.question_likert, self.general_contribution, [0, 0, 0, 0, 5], False),
*make_rating_answer_counters(self.question_likert_2, self.general_contribution, [0, 0, 3, 0, 0], False),
*make_rating_answer_counters(
self.question_negative_likert, self.general_contribution, [0, 0, 0, 4, 0], False
),
*make_rating_answer_counters(
self.question_bipolar, self.general_contribution, [0, 0, 0, 0, 0, 0, 2], False
),
Expand All @@ -235,7 +241,9 @@ def test_average_grade(self):
contributor2_average = 4
contributors_average = ((4 * contributor1_average) + (2 * contributor2_average)) / (4 + 2) # 2.9333333

general_non_grade_average = ((5 * 5) + (3 * 3) + (2 * 5) + (4 * 7 / 3)) / (5 + 3 + 2 + 4) # 3.80952380
general_non_grade_average = ((5 * 5) + (3 * 3) + (4 * 4) + (2 * 5) + (4 * 7 / 3)) / (
5 + 3 + 4 + 2 + 4
) # 3.85185185

contributors_percentage = settings.CONTRIBUTIONS_WEIGHT / (
settings.CONTRIBUTIONS_WEIGHT + settings.GENERAL_NON_GRADE_QUESTIONS_WEIGHT
Expand All @@ -246,11 +254,11 @@ def test_average_grade(self):

total_grade = (
contributors_percentage * contributors_average + general_non_grade_percentage * general_non_grade_average
) # 1.1 + 2.38095238 = 3.48095238
) # 1.1 + 2.4074074 = 3.5074074

average_grade = distribution_to_grade(calculate_average_distribution(self.evaluation))
self.assertAlmostEqual(average_grade, total_grade)
self.assertAlmostEqual(average_grade, 3.48095238)
self.assertAlmostEqual(average_grade, 3.5074074)

@override_settings(
CONTRIBUTOR_GRADE_QUESTIONS_WEIGHT=4,
Expand Down Expand Up @@ -464,7 +472,7 @@ def setUpTestData(cls):
)
cls.questionnaire = baker.make(Questionnaire)
cls.question = baker.make(Question, questionnaire=cls.questionnaire, type=QuestionType.TEXT)
cls.question_likert = baker.make(Question, questionnaire=cls.questionnaire, type=QuestionType.LIKERT)
cls.question_likert = baker.make(Question, questionnaire=cls.questionnaire, type=QuestionType.POSITIVE_LIKERT)
cls.general_contribution = cls.evaluation.general_contribution
cls.general_contribution.questionnaires.set([cls.questionnaire])
cls.responsible1_contribution = baker.make(
Expand Down
Loading

0 comments on commit e3c56c6

Please sign in to comment.