diff --git a/polls/admin.py b/polls/admin.py index ee1e244..3d70f89 100644 --- a/polls/admin.py +++ b/polls/admin.py @@ -13,12 +13,12 @@ class QuestionAdmin(admin.ModelAdmin): (None, {"fields": ["question_text"]}), ("Published date", {"fields": ["pub_date"], "classes": ["collapse"]}), ("End date", {"fields": ["end_date"], "classes": ["collapse"]}), - ("Vote count", {"fields": ["up_vote_count", "down_vote_count"]}), + ("Sentiment Vote count", {"fields": ["up_vote_count", "down_vote_count"]}), ("Participant count", {"fields": ["participant_count"]}), ] - list_display = ["question_text", "pub_date", "end_date", "was_published_recently"] + list_display = ["question_text", "pub_date", "end_date", "was_published_recently", "can_vote"] inlines = [ChoiceInline] - list_filter = ["pub_date"] + list_filter = ["pub_date", ] search_fields = ["question_text"] diff --git a/polls/migrations/0009_tag_remove_choice_votes_vote_question_tags.py b/polls/migrations/0009_tag_remove_choice_votes_vote_question_tags.py new file mode 100644 index 0000000..b724c1f --- /dev/null +++ b/polls/migrations/0009_tag_remove_choice_votes_vote_question_tags.py @@ -0,0 +1,41 @@ +# Generated by Django 4.2.4 on 2023-09-11 18:23 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('polls', '0008_alter_question_pub_date'), + ] + + operations = [ + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tag_text', models.CharField(max_length=50)), + ], + ), + migrations.RemoveField( + model_name='choice', + name='votes', + ), + migrations.CreateModel( + name='Vote', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('choice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polls.choice')), + ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polls.question')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='question', + name='tags', + field=models.ManyToManyField(blank=True, to='polls.tag'), + ), + ] diff --git a/polls/models.py b/polls/models.py index 24d3eb2..217b0bb 100644 --- a/polls/models.py +++ b/polls/models.py @@ -17,6 +17,16 @@ from django.db.models import Sum from django.contrib.auth.models import User +class Tag(models.Model): + """ + Represents a tag for a poll question. + """ + tag_text = models.CharField(max_length=50) + + def __str__(self): + return self.name + + class Question(models.Model): """ Represents a poll question. @@ -33,23 +43,15 @@ class Question(models.Model): """ question_text = models.CharField(max_length=100) - short_description = models.CharField(max_length=200, default="Cool kids have polls") - long_description = models.TextField( - max_length=2000, default="No description provide for this poll." - ) - pub_date = models.DateTimeField( - "date published", default=timezone.now, editable=True - ) + pub_date = models.DateTimeField("date published", default=timezone.now, editable=True) end_date = models.DateTimeField("date ended", null=True) - up_vote_count = models.PositiveIntegerField( - default=0, validators=[MinValueValidator(0), MaxValueValidator(2147483647)] - ) - down_vote_count = models.PositiveIntegerField( - default=0, validators=[MinValueValidator(0), MaxValueValidator(2147483647)] - ) - participant_count = models.PositiveIntegerField( - default=0, validators=[MinValueValidator(0), MaxValueValidator(2147483647)] - ) + short_description = models.CharField(max_length=200, default="Cool kids have polls") + long_description = models.TextField(max_length=2000, default="No description provide for this poll.") + tags = models.ManyToManyField(Tag, blank=True) + + up_vote_count = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0), MaxValueValidator(2147483647)]) + down_vote_count = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0), MaxValueValidator(2147483647)]) + participant_count = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0), MaxValueValidator(2147483647)]) def was_published_recently(self): """ @@ -151,6 +153,13 @@ class Question(models.Model): def down_vote_percentage(self): return self.calculate_vote_percentage()[1] + @property + def participants(self): + """ + Calculate the number of participants based on the number of votes. + """ + return self.vote_set.count() + class Choice(models.Model): """ @@ -164,40 +173,24 @@ class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) - votes = models.PositiveIntegerField( - default=0, validators=[MinValueValidator(0), MaxValueValidator(2147483647)] - ) - def tailwind_width_class(self): - """ - Calculate and return the Tailwind CSS width class based on the 'votes' percentage. - """ - total_votes = self.question.choice_set.aggregate(Sum("votes")).get( - "votes__sum", 0 - ) - #! Tailwind w-0 to w-48 - if total_votes == 0: - return "w-0" - - ratio = self.votes / total_votes - - scaled_value = ratio * 48 - - return f"w-{int(round(scaled_value))}" - - def calculate_percentage(self): - """Calculate percentage of votes for all choices.""" - total_votes_for_question = ( - self.question.choice_set.aggregate(Sum("votes"))["votes__sum"] or 0 - ) - - if total_votes_for_question == 0: - return 0 - else: - return round((self.votes / total_votes_for_question) * 100, 2) + @property + def votes(self): + return self.vote_set.count() def __str__(self): """ Returns a string representation of the choice. """ - return self.choice_text + return f"{self.choice_text} get ({self.votes})" + + +class Vote(models.Model): + """Represent Vote of User for a poll question.""" + + choice = models.ForeignKey(Choice, on_delete=models.CASCADE) + user = models.ForeignKey(User, on_delete=models.CASCADE) + question = models.ForeignKey(Question, on_delete=models.CASCADE) + + def __str__(self): + return f"{self.user} voted for {self.choice} in {self.question}" \ No newline at end of file diff --git a/polls/static/polls/js/detail.js b/polls/static/polls/js/detail.js index 86e7e5a..13c42a0 100644 --- a/polls/static/polls/js/detail.js +++ b/polls/static/polls/js/detail.js @@ -1,38 +1,82 @@ const toggleChoice = (button, choiceId) => { - const choiceInput = document.querySelector(`input[name="choice"][value="${choiceId}"]`); + const choiceInput = document.querySelector(`input[name="choice"][value="${choiceId}"]`); + const selectedChoice2 = document.getElementById("selected-choice-1"); + + if (selectedChoice2 !== null) { if (choiceInput) { - // already selected -> unselect it - if (choiceInput.checked) { - choiceInput.checked = false; - button.classList.remove('bg-green-500', 'border-solid', 'border-2', 'border-black', 'hover:bg-green-600'); - button.classList.add('bg-white', 'border-solid', 'border-2', 'border-black', 'hover:bg-white'); - // Clear display - document.getElementById('selected-choice').textContent = 'Please Select a Choice😊'; - } else { - // Unselect all choices - document.querySelectorAll('input[name="choice"]').forEach((choice) => { - choice.checked = false; - }); + // already selected -> unselect it + if (choiceInput.checked) { + choiceInput.checked = false; + button.classList.remove("bg-green-500", "border-solid", "border-2", "border-black", "hover:bg-green-600"); + button.classList.add("bg-white", "border-solid", "border-2", "border-black", "hover:bg-white"); + // Clear display + document.getElementById("selected-choice-1").textContent = "You have been voted😊 Anyway, you can change the choice anytime before end date"; + } else { + // Unselect all choices + document.querySelectorAll('input[name="choice"]').forEach(choice => { + choice.checked = false; + }); - // Select the clicked choice - choiceInput.checked = true; + // Select the clicked choice + choiceInput.checked = true; - // Reset the style of all choice buttons - document.querySelectorAll('.choice-button').forEach((btn) => { - btn.classList.remove('bg-green-500', 'border-solid', 'border-2', 'border-black', 'hover:bg-green-600'); - btn.classList.add('bg-white', 'border-solid', 'border-2', 'border-black', 'hover:bg-white'); - }); + // Reset the style of all choice buttons + document.querySelectorAll(".choice-button").forEach(btn => { + btn.classList.remove("bg-green-500", "border-solid", "border-2", "border-black", "hover:bg-green-600"); + btn.classList.add("bg-white", "border-solid", "border-2", "border-black", "hover:bg-white"); + }); - button.classList.remove('bg-white', 'border-solid', 'border-2', 'border-black', 'hover:bg-white'); - button.classList.add('bg-green-500', 'border-solid', 'border-2', 'border-black', 'hover:bg-green-600'); + button.classList.remove("bg-white", "border-solid", "border-2", "border-black", "hover:bg-white"); + button.classList.add("bg-green-500", "border-solid", "border-2", "border-black", "hover:bg-green-600"); - const choiceText = button.textContent.trim(); - document.getElementById('selected-choice').textContent = `You select: ${choiceText}`; - } - - // Enable the "Vote" button -> if select - const voteButton = document.getElementById('vote-button'); - voteButton.disabled = !document.querySelector('input[name="choice"]:checked'); + const choiceText = button.textContent.trim(); + document.getElementById("selected-choice-1").textContent = `You select: ${choiceText}`; + } } -}; \ No newline at end of file + // Enable the "Vote" button -> if select + const voteButton = document.getElementById("vote-button"); + voteButton.disabled = !document.querySelector('input[name="choice"]:checked'); + } else { + if (choiceInput) { + // already selected -> unselect it + if (choiceInput.checked) { + choiceInput.checked = false; + button.classList.remove("bg-green-500", "border-solid", "border-2", "border-black", "hover:bg-green-600"); + button.classList.add("bg-white", "border-solid", "border-2", "border-black", "hover:bg-white"); + // Clear display + document.getElementById("selected-choice-2").textContent = "Please Select a Choice😊"; + } else { + // Unselect all choices + document.querySelectorAll('input[name="choice"]').forEach(choice => { + choice.checked = false; + }); + + // Select the clicked choice + choiceInput.checked = true; + + // Reset the style of all choice buttons + document.querySelectorAll(".choice-button").forEach(btn => { + btn.classList.remove("bg-green-500", "border-solid", "border-2", "border-black", "hover:bg-green-600"); + btn.classList.add("bg-white", "border-solid", "border-2", "border-black", "hover:bg-white"); + }); + + button.classList.remove("bg-white", "border-solid", "border-2", "border-black", "hover:bg-white"); + button.classList.add("bg-green-500", "border-solid", "border-2", "border-black", "hover:bg-green-600"); + + const choiceText = button.textContent.trim(); + document.getElementById("selected-choice-2").textContent = `You select: ${choiceText}`; + } + } + // Enable the "Vote" button -> if select + const voteButton = document.getElementById("vote-button"); + voteButton.disabled = !document.querySelector('input[name="choice"]:checked'); + } +}; + +function confirmChangeVote(choiceId) { + const confirmation = confirm("Are you sure you want to change your vote?"); + if (confirmation) { + window.location.href = `{% url 'polls:vote' question.id %}${choiceId}`; + } +} diff --git a/polls/templates/polls/detail.html b/polls/templates/polls/detail.html index e08d9f8..08614ea 100644 --- a/polls/templates/polls/detail.html +++ b/polls/templates/polls/detail.html @@ -46,14 +46,29 @@