diff --git a/README.md b/README.md index fad8e87..5aef873 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,15 @@ python -m virtualenv .venv |:--:|:--:| |admin|ineedmorebullets| +## Demo User + +|Username|Password| +|:--:|:--:| +|tester1|aa12345678aa| +|tester2|aa12345678aa| +|tester3|aa12345678aa| +|tester4|aa12345678aa| + ## Project Documents All project documents are in the [Project Wiki](https://github.com/Sosokker/ku-polls/wiki). diff --git a/data/polls.json b/data/polls.json new file mode 100644 index 0000000..4935eaa --- /dev/null +++ b/data/polls.json @@ -0,0 +1,242 @@ +[ +{ + "model": "polls.question", + "pk": 1, + "fields": { + "question_text": "Python vs C++, which one is better in your opinion?", + "pub_date": "2023-09-05T06:31:14Z", + "end_date": "2023-09-29T20:31:49Z", + "short_description": "Cool kids have polls", + "long_description": "No description provide for this poll.", + "up_vote_count": 5, + "down_vote_count": 0, + "participant_count": 6, + "tags": [] + } +}, +{ + "model": "polls.question", + "pk": 2, + "fields": { + "question_text": "The chicken and the egg, which came first?", + "pub_date": "2023-09-11T02:50:04Z", + "end_date": "2023-09-19T23:50:19Z", + "short_description": "Cool kids have polls", + "long_description": "No description provide for this poll.", + "up_vote_count": 1, + "down_vote_count": 0, + "participant_count": 0, + "tags": [] + } +}, +{ + "model": "polls.question", + "pk": 3, + "fields": { + "question_text": "So far so good?", + "pub_date": "2023-08-03T06:50:43Z", + "end_date": "2023-11-15T19:50:53Z", + "short_description": "Cool kids have polls", + "long_description": "No description provide for this poll.", + "up_vote_count": 1, + "down_vote_count": 0, + "participant_count": 0, + "tags": [] + } +}, +{ + "model": "polls.question", + "pk": 4, + "fields": { + "question_text": "Do you love Django?", + "pub_date": "2023-09-11T19:51:12Z", + "end_date": "2023-09-13T17:51:18Z", + "short_description": "Cool kids have polls", + "long_description": "No description provide for this poll.", + "up_vote_count": 10, + "down_vote_count": 0, + "participant_count": 0, + "tags": [] + } +}, +{ + "model": "polls.choice", + "pk": 1, + "fields": { + "question": 1, + "choice_text": "C++" + } +}, +{ + "model": "polls.choice", + "pk": 2, + "fields": { + "question": 1, + "choice_text": "Python" + } +}, +{ + "model": "polls.choice", + "pk": 3, + "fields": { + "question": 2, + "choice_text": "Egg!" + } +}, +{ + "model": "polls.choice", + "pk": 4, + "fields": { + "question": 2, + "choice_text": "Chicken" + } +}, +{ + "model": "polls.choice", + "pk": 5, + "fields": { + "question": 3, + "choice_text": "Yes sir!" + } +}, +{ + "model": "polls.choice", + "pk": 6, + "fields": { + "question": 3, + "choice_text": "Nah" + } +}, +{ + "model": "polls.choice", + "pk": 7, + "fields": { + "question": 4, + "choice_text": "Yeah for sure!" + } +}, +{ + "model": "polls.choice", + "pk": 8, + "fields": { + "question": 4, + "choice_text": "Hell nah!" + } +}, +{ + "model": "polls.choice", + "pk": 9, + "fields": { + "question": 4, + "choice_text": "No comment." + } +}, +{ + "model": "polls.vote", + "pk": 1, + "fields": { + "choice": 1, + "user": 1, + "question": 1 + } +}, +{ + "model": "polls.vote", + "pk": 2, + "fields": { + "choice": 2, + "user": 3, + "question": 1 + } +}, +{ + "model": "polls.vote", + "pk": 3, + "fields": { + "choice": 1, + "user": 2, + "question": 1 + } +}, +{ + "model": "polls.vote", + "pk": 4, + "fields": { + "choice": 7, + "user": 2, + "question": 4 + } +}, +{ + "model": "polls.vote", + "pk": 5, + "fields": { + "choice": 6, + "user": 2, + "question": 3 + } +}, +{ + "model": "polls.vote", + "pk": 6, + "fields": { + "choice": 4, + "user": 2, + "question": 2 + } +}, +{ + "model": "polls.vote", + "pk": 7, + "fields": { + "choice": 8, + "user": 4, + "question": 4 + } +}, +{ + "model": "polls.vote", + "pk": 8, + "fields": { + "choice": 6, + "user": 4, + "question": 3 + } +}, +{ + "model": "polls.vote", + "pk": 9, + "fields": { + "choice": 3, + "user": 4, + "question": 2 + } +}, +{ + "model": "polls.vote", + "pk": 10, + "fields": { + "choice": 2, + "user": 4, + "question": 1 + } +}, +{ + "model": "polls.vote", + "pk": 11, + "fields": { + "choice": 6, + "user": 3, + "question": 3 + } +}, +{ + "model": "polls.vote", + "pk": 12, + "fields": { + "choice": 3, + "user": 3, + "question": 2 + } +} +] diff --git a/data/user.json b/data/user.json deleted file mode 100644 index 6afa9b8..0000000 --- a/data/user.json +++ /dev/null @@ -1,20 +0,0 @@ -[ -{ - "model": "auth.user", - "pk": 1, - "fields": { - "password": "pbkdf2_sha256$600000$iGfCFe97r89Z86pdlQGUnB$WWlHxi2Q1iVSSk0kZt0C5QSwwBKwzEofhJ8CBvakemU=", - "last_login": null, - "is_superuser": true, - "username": "admin", - "first_name": "", - "last_name": "", - "email": "admin@email.com", - "is_staff": true, - "is_active": true, - "date_joined": "2023-08-31T10:14:33.235Z", - "groups": [], - "user_permissions": [] - } -} -] diff --git a/data/users.json b/data/users.json new file mode 100644 index 0000000..cc88812 --- /dev/null +++ b/data/users.json @@ -0,0 +1,92 @@ +[ +{ + "model": "auth.user", + "pk": 1, + "fields": { + "password": "pbkdf2_sha256$600000$aDh9a1PXxcXAb8z3YIjAPX$NVH24kt/wMad+0fZcCii738dfojI4vL2ffXOwNRuLz4=", + "last_login": "2023-09-12T04:02:42.758Z", + "is_superuser": true, + "username": "admin", + "first_name": "", + "last_name": "", + "email": "admin@email.com", + "is_staff": true, + "is_active": true, + "date_joined": "2023-09-11T18:24:20.740Z", + "groups": [], + "user_permissions": [] + } +}, +{ + "model": "auth.user", + "pk": 2, + "fields": { + "password": "pbkdf2_sha256$600000$quZKLKT8Ec3TQgpdqlCkpX$o+VOOnRDLGf64qjHb239Yvsre74tPkC8hw1qH1un/hk=", + "last_login": "2023-09-12T04:22:38.555Z", + "is_superuser": false, + "username": "tester1", + "first_name": "", + "last_name": "", + "email": "", + "is_staff": false, + "is_active": true, + "date_joined": "2023-09-11T19:41:22.592Z", + "groups": [], + "user_permissions": [] + } +}, +{ + "model": "auth.user", + "pk": 3, + "fields": { + "password": "pbkdf2_sha256$600000$1xGp6EDCoaljdTlSdVT1Mn$UID0Woeh8hwW7LtchH+hKzqdKTDeITTxQ/0DGvfG3CY=", + "last_login": "2023-09-11T19:57:39.303Z", + "is_superuser": false, + "username": "tester3", + "first_name": "", + "last_name": "", + "email": "", + "is_staff": false, + "is_active": true, + "date_joined": "2023-09-11T19:41:41.209Z", + "groups": [], + "user_permissions": [] + } +}, +{ + "model": "auth.user", + "pk": 4, + "fields": { + "password": "pbkdf2_sha256$600000$fJJcIwAuIESYwZDBOqBv8t$YEDVCgg/xJOqAOiAdvGvvqgi1jgn1YfYHJE9yx2JWTA=", + "last_login": "2023-09-11T19:55:41.583Z", + "is_superuser": false, + "username": "tester2", + "first_name": "", + "last_name": "", + "email": "", + "is_staff": false, + "is_active": true, + "date_joined": "2023-09-11T19:43:25.226Z", + "groups": [], + "user_permissions": [] + } +}, +{ + "model": "auth.user", + "pk": 5, + "fields": { + "password": "pbkdf2_sha256$600000$aHyU2gjOR6Vfsh3DBMIvQy$PZwRu+rOLc+N15DDguvy29dks6GUiN5YN/4io8b390o=", + "last_login": null, + "is_superuser": false, + "username": "novote", + "first_name": "", + "last_name": "", + "email": "", + "is_staff": false, + "is_active": true, + "date_joined": "2023-09-11T19:52:38.130Z", + "groups": [], + "user_permissions": [] + } +} +] diff --git a/mysite/settings.py b/mysite/settings.py index 572a80b..f5523db 100644 --- a/mysite/settings.py +++ b/mysite/settings.py @@ -126,3 +126,9 @@ STATICFILES_DIRS = [BASE_DIR] # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +LOGIN_REDIRECT_URL = "home_redirect" +LOGOUT_REDIRECT_URL = "home_redirect" + +EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend" +EMAIL_FILE_PATH = BASE_DIR / "sent_emails" \ No newline at end of file diff --git a/mysite/urls.py b/mysite/urls.py index 1b67d3f..5e952b1 100644 --- a/mysite/urls.py +++ b/mysite/urls.py @@ -7,4 +7,5 @@ urlpatterns = [ path('', RedirectView.as_view(pattern_name='polls:index'), name='home_redirect'), path("polls/", include("polls.urls")), path('admin/', admin.site.urls), + path("accounts/", include("django.contrib.auth.urls")), ] diff --git a/polls/admin.py b/polls/admin.py index ee1e244..66a4b82 100644 --- a/polls/admin.py +++ b/polls/admin.py @@ -13,12 +13,11 @@ 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"]}), - ("Participant count", {"fields": ["participant_count"]}), + ("Sentiment Vote count", {"fields": ["up_vote_count", "down_vote_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/forms.py b/polls/forms.py new file mode 100644 index 0000000..1e738ea --- /dev/null +++ b/polls/forms.py @@ -0,0 +1,27 @@ +from django import forms +from django.contrib.auth.forms import UserCreationForm +from django.contrib.auth.models import User + + +class SignUpForm(UserCreationForm): + tailwind_class = "w-full border-2 border-gray-300 bg-gray-100 rounded-lg focus:ring focus:border-blue-300 focus:shadow-none" + + username = forms.CharField(widget=forms.TextInput(attrs={'class': tailwind_class}), + error_messages={ + 'unique': 'This username is already in use.', + 'invalid': 'Invalid username format.', + 'max_length': 'Username should not exceed 150 characters.', + } + ) + password1 = forms.CharField(widget=forms.PasswordInput(attrs={'class': tailwind_class}), + error_messages={'min_length': 'Password must contain at least 8 characters.',} + ) + password2 = forms.CharField(widget=forms.PasswordInput(attrs={'class': tailwind_class}),) + + class Meta: + model = User + fields = ('username', 'password1', 'password2') + + error_messages = { + 'password_mismatch': "The two password fields didn't match.", + } \ No newline at end of file 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 1e06a4e..217b0bb 100644 --- a/polls/models.py +++ b/polls/models.py @@ -14,6 +14,17 @@ from django.utils import timezone from django.contrib import admin from django.core.validators import MaxValueValidator, MinValueValidator 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): @@ -32,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): """ @@ -150,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): """ @@ -163,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 6778f30..08614ea 100644 --- a/polls/templates/polls/detail.html +++ b/polls/templates/polls/detail.html @@ -16,9 +16,8 @@ - -
{{ question.short_description }}