From 504aa66d6323e811dbf2c0b182bbe3165db138bf Mon Sep 17 00:00:00 2001 From: AlvaroRodriguezGarcia-hue Date: Tue, 14 Nov 2023 21:03:44 +0100 Subject: [PATCH 01/11] feat: boolean voting --- decide/voting/models.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/decide/voting/models.py b/decide/voting/models.py index 5ca075589..68f963ef7 100644 --- a/decide/voting/models.py +++ b/decide/voting/models.py @@ -7,10 +7,7 @@ class Question(models.Model): - QUESTION_TYPES = ( - ("S", "Single"), - ("M", "Multiple"), - ) + QUESTION_TYPES = (("S", "Single"), ("M", "Multiple"), ("B", "Boolean")) question_type = models.CharField(max_length=1, choices=QUESTION_TYPES, default="S") desc = models.TextField() @@ -26,10 +23,12 @@ class QuestionOption(models.Model): number = models.PositiveIntegerField(blank=True, null=True) option = models.TextField() - def save(self): + def save(self, *args, **kwargs): + if self.question.question_type == "B" and self.question.options.count() > 1: + raise ValueError("Boolean questions can only have two options.") if not self.number: self.number = self.question.options.count() + 2 - return super().save() + super().save(*args, **kwargs) def __str__(self): return "{} ({})".format(self.option, self.number) From f09bbd48fbd68560895a3085943aecefd30d832f Mon Sep 17 00:00:00 2001 From: pabpercab1 Date: Thu, 14 Dec 2023 12:14:31 +0100 Subject: [PATCH 02/11] test(voting): Creations of the boolean tests --- decide/voting/tests.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/decide/voting/tests.py b/decide/voting/tests.py index 1290cdb02..48867d90d 100644 --- a/decide/voting/tests.py +++ b/decide/voting/tests.py @@ -456,3 +456,36 @@ def createCensusEmptyError(self): self.cleaner.current_url == self.live_server_url + "/admin/voting/question/add/" ) + + +class TestBooleanVoting(BaseTestCase): + def setUp(self): + super().setUp() + + def tearDown(self): + super().tearDown() + self.assertTrue(True, False) + + def encrypt_msg(self, msg, v, bits=settings.KEYBITS): + pk = v.pub_key + p, g, y = (pk.p, pk.g, pk.y) + k = MixCrypt(bits=bits) + k.k = ElGamal.construct((p, g, y)) + return k.encrypt(msg) + + def create_voting(self): + q = Question(desc="test question") + q.save() + for i in range(5): + opt = QuestionOption(question=q, option="option {}".format(i + 1)) + opt.save() + v = Voting(name="test voting", question=q) + v.save() + + a, _ = Auth.objects.get_or_create( + url=settings.BASEURL, defaults={"me": True, "name": "test auth"} + ) + a.save() + v.auths.add(a) + + return v From 8aa837c1de81653fc995212dc63500f3eeb0092f Mon Sep 17 00:00:00 2001 From: pabpercab1 Date: Thu, 14 Dec 2023 20:23:07 +0100 Subject: [PATCH 03/11] test(voting): All test for boolean voting feature --- decide/voting/tests.py | 82 +++++++++++++++++++++++++++--------------- 1 file changed, 54 insertions(+), 28 deletions(-) diff --git a/decide/voting/tests.py b/decide/voting/tests.py index 48867d90d..380910e44 100644 --- a/decide/voting/tests.py +++ b/decide/voting/tests.py @@ -136,6 +136,32 @@ def test_create_voting_from_api(self): response = self.client.post("/voting/", data, format="json") self.assertEqual(response.status_code, 201) + def test_create_boolean_voting_from_api(self): + data = {"name": "Example boolean"} + response = self.client.post("/voting/", data, format="json") + self.assertEqual(response.status_code, 401) + + # login with user no admin + self.login(user="noadmin") + response = mods.post("voting", params=data, response=True) + self.assertEqual(response.status_code, 403) + + # login with user admin + self.login() + response = mods.post("voting", params=data, response=True) + self.assertEqual(response.status_code, 400) + + data = { + "name": "Example boolean", + "desc": "Description example boolean", + "question_type": "B", + "question": "I want a ", + "question_opt": ["cat", "dog"], + } + + response = self.client.post("/voting/", data, format="json") + self.assertEqual(response.status_code, 201) + def test_update_voting(self): voting = self.create_voting() @@ -458,34 +484,34 @@ def createCensusEmptyError(self): ) -class TestBooleanVoting(BaseTestCase): +class QuestionOptionTestCase(TestCase): def setUp(self): - super().setUp() - - def tearDown(self): - super().tearDown() - self.assertTrue(True, False) - - def encrypt_msg(self, msg, v, bits=settings.KEYBITS): - pk = v.pub_key - p, g, y = (pk.p, pk.g, pk.y) - k = MixCrypt(bits=bits) - k.k = ElGamal.construct((p, g, y)) - return k.encrypt(msg) - - def create_voting(self): - q = Question(desc="test question") - q.save() - for i in range(5): - opt = QuestionOption(question=q, option="option {}".format(i + 1)) - opt.save() - v = Voting(name="test voting", question=q) - v.save() - - a, _ = Auth.objects.get_or_create( - url=settings.BASEURL, defaults={"me": True, "name": "test auth"} + self.question = Question.objects.create(question_type="B") + self.option1 = QuestionOption.objects.create( + question=self.question, option="Option 1" + ) + self.option2 = QuestionOption.objects.create( + question=self.question, option="Option 2" ) - a.save() - v.auths.add(a) - return v + def test_save_method_with_boolean_question_and_multiple_options(self): + with self.assertRaises(ValueError): + option3 = QuestionOption(question=self.question, option="Option 3") + option3.save() + + def test_save_method_with_boolean_question_and_single_option(self): + self.question.options.all().delete() + option3 = QuestionOption(question=self.question, option="Option 3") + option3.save() + self.assertEqual(option3.number, 2) + + def test_save_method_with_non_boolean_question(self): + self.question.question_type = "S" + self.question.save() + option3 = QuestionOption(question=self.question, option="Option 3") + option3.save() + self.assertEqual(option3.number, 4) + + def test_str_method(self): + self.assertEqual(str(self.option1), "Option 1 (2)") + self.assertEqual(str(self.option2), "Option 2 (3)") From 5529c9f4722a5a1ef93f701b063c61c2da27b947 Mon Sep 17 00:00:00 2001 From: AlvaroRodriguezGarcia-hue Date: Fri, 15 Dec 2023 18:47:36 +0100 Subject: [PATCH 04/11] fix: error code for 3+ options restriction --- decide/voting/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/decide/voting/models.py b/decide/voting/models.py index 68f963ef7..829490a7f 100644 --- a/decide/voting/models.py +++ b/decide/voting/models.py @@ -4,6 +4,7 @@ from django.db.models import JSONField from django.db.models.signals import post_save from django.dispatch import receiver +from django.core.exceptions import BadRequest class Question(models.Model): @@ -25,7 +26,7 @@ class QuestionOption(models.Model): def save(self, *args, **kwargs): if self.question.question_type == "B" and self.question.options.count() > 1: - raise ValueError("Boolean questions can only have two options.") + raise BadRequest("Boolean questions can only have two options.") if not self.number: self.number = self.question.options.count() + 2 super().save(*args, **kwargs) From fbe757992a56ffdc3b07b36d202a9d4f3189fa73 Mon Sep 17 00:00:00 2001 From: AlvaroRodriguezGarcia-hue Date: Fri, 15 Dec 2023 19:45:18 +0100 Subject: [PATCH 05/11] Fixing merge errors --- decide/voting/models.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/decide/voting/models.py b/decide/voting/models.py index 800ca8148..1bcfa72d9 100644 --- a/decide/voting/models.py +++ b/decide/voting/models.py @@ -44,20 +44,15 @@ class QuestionOption(models.Model): option = models.TextField() hidden = models.BooleanField(default=False) - def save(self, *args, **kwargs): + def save(self): if self.question.question_type == "B" and self.question.options.count() > 1: raise BadRequest("Boolean questions can only have two options.") if not self.number: -<<<<<<< HEAD - self.number = self.question.options.count() + 2 - super().save(*args, **kwargs) -======= self.number = self.question.options.count() + 1 else: if self.number and Question.voteBlank: self.number = self.question.options.count() + 1 return super().save() ->>>>>>> develop def __str__(self): return "{} ({})".format(self.option, self.number) From a000e0cf33acf660892f8865a836501fbefe03c9 Mon Sep 17 00:00:00 2001 From: AlvaroRodriguezGarcia-hue Date: Fri, 15 Dec 2023 21:02:59 +0100 Subject: [PATCH 06/11] feat: new boolean voting --- decide/voting/models.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/decide/voting/models.py b/decide/voting/models.py index 1bcfa72d9..d481e7977 100644 --- a/decide/voting/models.py +++ b/decide/voting/models.py @@ -30,6 +30,29 @@ def save(self, **kwargs): ) enBlanco.save() self.options.add(enBlanco) + if self.question_type == "B": + if ( + QuestionOption.objects.filter( + question__id=self.id, option__startswith="Sí" + ).count() + == 0 + ): + op1 = QuestionOption( + question=self, number=self.options.count() + 1, option="Sí" + ) + op1.save() + self.options.add(op1) + if ( + QuestionOption.objects.filter( + question__id=self.id, option__startswith="No" + ).count() + == 0 + ): + op2 = QuestionOption( + question=self, number=self.options.count() + 1, option="No" + ) + op2.save() + self.options.add(op2) return super().save() def __str__(self): @@ -46,7 +69,7 @@ class QuestionOption(models.Model): def save(self): if self.question.question_type == "B" and self.question.options.count() > 1: - raise BadRequest("Boolean questions can only have two options.") + raise BadRequest("Boolean questions cannot have any options.") if not self.number: self.number = self.question.options.count() + 1 else: From edf9f1db81dc831a9de3bdd372c3eaac2d1550a2 Mon Sep 17 00:00:00 2001 From: pabpercab1 Date: Sat, 16 Dec 2023 16:26:09 +0100 Subject: [PATCH 07/11] fix(voting): Corrected boolean issue with white --- .../0011_alter_question_question_type.py | 18 +++++++++++++ decide/voting/models.py | 26 ++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 decide/voting/migrations/0011_alter_question_question_type.py diff --git a/decide/voting/migrations/0011_alter_question_question_type.py b/decide/voting/migrations/0011_alter_question_question_type.py new file mode 100644 index 000000000..4e149e944 --- /dev/null +++ b/decide/voting/migrations/0011_alter_question_question_type.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1 on 2023-12-16 13:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('voting', '0010_question_voteblank'), + ] + + operations = [ + migrations.AlterField( + model_name='question', + name='question_type', + field=models.CharField(choices=[('S', 'Single'), ('M', 'Multiple'), ('B', 'Boolean')], default='S', max_length=1), + ), + ] diff --git a/decide/voting/models.py b/decide/voting/models.py index d481e7977..0da673ac4 100644 --- a/decide/voting/models.py +++ b/decide/voting/models.py @@ -53,6 +53,20 @@ def save(self, **kwargs): ) op2.save() self.options.add(op2) + if ( + self.voteBlank + and QuestionOption.objects.filter( + question_id=self.id, option__startswith="Voto En Blanco" + ).count() + == 0 + ): + enBlanco = QuestionOption( + question=self, + number=self.options.count() + 1, + option="Voto En Blanco", + ) + enBlanco.save() + self.options.add(enBlanco) return super().save() def __str__(self): @@ -69,7 +83,17 @@ class QuestionOption(models.Model): def save(self): if self.question.question_type == "B" and self.question.options.count() > 1: - raise BadRequest("Boolean questions cannot have any options.") + if self.question.voteBlank: + if ( + self.option != "Sí" + and self.option != "No" + and self.option != "Voto En Blanco" + ): + raise BadRequest( + "Boolean questions with white votes can only have 'Sí', 'No', or 'Voto En Blanco' options." + ) + else: + raise BadRequest("Boolean questions cannot have any options.") if not self.number: self.number = self.question.options.count() + 1 else: From 135e4141884dddace64d33dae6f9369c519d7a89 Mon Sep 17 00:00:00 2001 From: pabpercab1 Date: Sat, 16 Dec 2023 16:26:31 +0100 Subject: [PATCH 08/11] test(voting): Corrected test for boolean --- decide/voting/tests.py | 68 ++++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 23 deletions(-) diff --git a/decide/voting/tests.py b/decide/voting/tests.py index b7fe9d141..5e1e436ad 100644 --- a/decide/voting/tests.py +++ b/decide/voting/tests.py @@ -14,6 +14,7 @@ from selenium.webdriver.common.by import By from voting.models import Question, QuestionOption, Voting from selenium.webdriver.chrome.webdriver import WebDriver +from django.core.exceptions import BadRequest class VotingTestCase(BaseTestCase): @@ -619,34 +620,55 @@ def createCensusEmptyError(self): ) -class QuestionOptionTestCase(TestCase): +class BooleanQuestionTestCase(BaseTestCase): def setUp(self): - self.question = Question.objects.create(question_type="B") - self.option1 = QuestionOption.objects.create( - question=self.question, option="Option 1" + self.question = Question.objects.create( + question_type="B", desc="What is your favorite color?", voteBlank=False ) - self.option2 = QuestionOption.objects.create( - question=self.question, option="Option 2" + self.question.save() + + def test_save_boolean_question(self): + self.assertEqual(QuestionOption.objects.count(), 2) + self.assertEqual(QuestionOption.objects.first().option, "Sí") + self.assertEqual(QuestionOption.objects.last().option, "No") + + def test_str_representation(self): + self.assertEqual(str(self.question), "What is your favorite color?") + + def test_one_option_boolean_question(self): + self.question = Question.objects.create( + question_type="B", desc="What is your favorite color?", voteBlank=False + ) + self.question.save() + + opt = QuestionOption(question=self.question, option="option {}".format(1)) + with self.assertRaises(BadRequest): + opt.save() + + def test_multiple_option_boolean_question(self): + self.question = Question.objects.create( + question_type="B", desc="What is your favorite color?", voteBlank=False ) + self.question.save() - def test_save_method_with_boolean_question_and_multiple_options(self): - with self.assertRaises(ValueError): - option3 = QuestionOption(question=self.question, option="Option 3") - option3.save() + opt1 = QuestionOption(question=self.question, option="option {}".format(1)) + opt2 = QuestionOption(question=self.question, option="option {}".format(2)) + opt3 = QuestionOption(question=self.question, option="option {}".format(3)) + with self.assertRaises(BadRequest): + opt1.save() + opt2.save() + opt3.save() - def test_save_method_with_boolean_question_and_single_option(self): - self.question.options.all().delete() - option3 = QuestionOption(question=self.question, option="Option 3") - option3.save() - self.assertEqual(option3.number, 2) - def test_save_method_with_non_boolean_question(self): - self.question.question_type = "S" +class BooleanWhiteTestCase(BaseTestCase): + def setUp(self): + self.question = Question.objects.create( + question_type="B", desc="What is your favorite color?", voteBlank=True + ) self.question.save() - option3 = QuestionOption(question=self.question, option="Option 3") - option3.save() - self.assertEqual(option3.number, 4) - def test_str_method(self): - self.assertEqual(str(self.option1), "Option 1 (2)") - self.assertEqual(str(self.option2), "Option 2 (3)") + def test_boolean_question_and_white(self): + self.assertEqual(QuestionOption.objects.count(), 3) + self.assertEqual(QuestionOption.objects.first().option, "Sí") + self.assertEqual(QuestionOption.objects.all()[1].option, "No") + self.assertEqual(QuestionOption.objects.last().option, "Voto En Blanco") From 3251a25a25c0bc5eefc238d35c37245d6508a1cb Mon Sep 17 00:00:00 2001 From: AlvaroRodriguezGarcia-hue Date: Sat, 16 Dec 2023 16:08:53 +0000 Subject: [PATCH 09/11] refactor: Format code with Black --- .../migrations/0011_alter_question_question_type.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/decide/voting/migrations/0011_alter_question_question_type.py b/decide/voting/migrations/0011_alter_question_question_type.py index 4e149e944..23f5d13b1 100644 --- a/decide/voting/migrations/0011_alter_question_question_type.py +++ b/decide/voting/migrations/0011_alter_question_question_type.py @@ -4,15 +4,18 @@ class Migration(migrations.Migration): - dependencies = [ - ('voting', '0010_question_voteblank'), + ("voting", "0010_question_voteblank"), ] operations = [ migrations.AlterField( - model_name='question', - name='question_type', - field=models.CharField(choices=[('S', 'Single'), ('M', 'Multiple'), ('B', 'Boolean')], default='S', max_length=1), + model_name="question", + name="question_type", + field=models.CharField( + choices=[("S", "Single"), ("M", "Multiple"), ("B", "Boolean")], + default="S", + max_length=1, + ), ), ] From 9ee6629e981f8cdb0352f380e9ce92b19dacf090 Mon Sep 17 00:00:00 2001 From: pabpercab1 Date: Sat, 16 Dec 2023 20:12:48 +0100 Subject: [PATCH 10/11] fix(voting): Test regarding boolean from api --- decide/voting/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/decide/voting/tests.py b/decide/voting/tests.py index 5e1e436ad..e87460b4b 100644 --- a/decide/voting/tests.py +++ b/decide/voting/tests.py @@ -258,8 +258,8 @@ def test_create_boolean_voting_from_api(self): "name": "Example boolean", "desc": "Description example boolean", "question_type": "B", - "question": "I want a ", - "question_opt": ["cat", "dog"], + "question": "Do you agree?", + "question_opt": ["Sí", "No"], } response = self.client.post("/voting/", data, format="json") From 6e8fe9c59d5d513d0640489f974a6065ee1dc66f Mon Sep 17 00:00:00 2001 From: pabpercab1 Date: Sat, 16 Dec 2023 20:30:18 +0100 Subject: [PATCH 11/11] fix(store): Handle boolean voting --- decide/store/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/decide/store/views.py b/decide/store/views.py index d279d87e5..016bbd39c 100644 --- a/decide/store/views.py +++ b/decide/store/views.py @@ -76,7 +76,10 @@ def post(self, request): return Response({}, status=status.HTTP_401_UNAUTHORIZED) with transaction.atomic(): - if voting[0]["question"]["question_type"] == "S": + if ( + voting[0]["question"]["question_type"] == "S" + or voting[0]["question"]["question_type"] == "B" + ): v, _ = Vote.objects.get_or_create(voting_id=vid, voter_id=uid) # Delete previous options VoteOption.objects.filter(vote=v).delete()