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 @@ - -
+ {% comment %}
@@ -33,10 +32,10 @@
-
+ {% endcomment %} -
+
@@ -47,19 +46,29 @@
{% csrf_token %} -
-
Please Select a Choice😊
-
- {% if error_message %} -
-

{{ error_message }}

-
+ {% if selected_choice %} +
+
You have been voted: {{ selected_choice.choice_text }}
+
+ {% else %} +
+
Please Select a Choice😊
+
{% endif %}
{% for choice in question.choice_set.all %} {% endfor %}
- + diff --git a/polls/templates/polls/index.html b/polls/templates/polls/index.html index ff23cdc..d870900 100644 --- a/polls/templates/polls/index.html +++ b/polls/templates/polls/index.html @@ -26,15 +26,22 @@ -
- + {% if user.is_authenticated %} + + + Sign out + + {% else %} + Sign in + {% endif %}
@@ -75,7 +82,7 @@
-

{{ question.question_text }}

+

{{ question.question_text }}


{{ question.short_description }}

@@ -88,7 +95,7 @@
🕒 {{ question.time_left }} - {{ question.participant_count }} Participants 👤 + {{ question.participants }} Participants 👤
+ +

+ Don't have an account? Sign up here +

+

+ Forget the Password? Reset here +

+ Back to Poll +
+ + diff --git a/polls/templates/registration/password_reset_complete.html b/polls/templates/registration/password_reset_complete.html new file mode 100644 index 0000000..066a37a --- /dev/null +++ b/polls/templates/registration/password_reset_complete.html @@ -0,0 +1,2 @@ +

Password reset complete

+

Your new password has been set. You can log in now on the log in page.

\ No newline at end of file diff --git a/polls/templates/registration/password_reset_confirm.html b/polls/templates/registration/password_reset_confirm.html new file mode 100644 index 0000000..c3d1b37 --- /dev/null +++ b/polls/templates/registration/password_reset_confirm.html @@ -0,0 +1,14 @@ +{% if validlink %} + +

Set a new password!

+
+ {% csrf_token %} + {{ form.as_p }} + +
+ +{% else %} + +

The password reset link was invalid, possibly because it has already been used. Please request a new password reset.

+ +{% endif %} \ No newline at end of file diff --git a/polls/templates/registration/password_reset_done.html b/polls/templates/registration/password_reset_done.html new file mode 100644 index 0000000..fa1ed5a --- /dev/null +++ b/polls/templates/registration/password_reset_done.html @@ -0,0 +1,2 @@ +

Check your inbox.

+

We've emailed you instructions for setting your password. You should receive the email shortly!

\ No newline at end of file diff --git a/polls/templates/registration/password_reset_form.html b/polls/templates/registration/password_reset_form.html new file mode 100644 index 0000000..98b091c --- /dev/null +++ b/polls/templates/registration/password_reset_form.html @@ -0,0 +1,8 @@ +

Forgot your password?

+

Enter your email address below, and we'll email instructions for setting a new one.

+ +
+ {% csrf_token %} + {{ form.as_p }} + +
\ No newline at end of file diff --git a/polls/templates/registration/signup.html b/polls/templates/registration/signup.html new file mode 100644 index 0000000..0bac13d --- /dev/null +++ b/polls/templates/registration/signup.html @@ -0,0 +1,46 @@ + + + + + Sign Up Page + + + +
+
+

Sign Up

+
+ {% csrf_token %} +
+

Username

+ {{ form.username }} +
+
+

Password

+ {{ form.password1 }} +
+

Password Confirmation

+ {{ form.password2 }} +
+ + +

Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.

+

Your password can’t be too similar to your other personal information. , must contain at least 8 characters, can’t be entirely numeric.

+ +
+ {% if form.errors %} + {% for field in form %} + {% if field.errors %} + {% for error in field.errors %} +

{{ error }}

+ {% endfor %} + {% endif %} + {% endfor %} + {% endif %} +
+

Already have an account? Sign in here

+ Back to Poll +
+
+ + \ No newline at end of file diff --git a/polls/tests.py b/polls/tests.py index c21dd77..db9a67f 100644 --- a/polls/tests.py +++ b/polls/tests.py @@ -3,6 +3,7 @@ import datetime from django.test import TestCase from django.utils import timezone from django.urls import reverse +from django.contrib.auth.models import User from .models import Question @@ -199,6 +200,9 @@ class QuestionDetailViewTests(TestCase): future_question.pub_date = timezone.now() + timezone.timedelta(days=5) future_question.save() + user = User.objects.create_user(username="testcase", password="123test123") + self.client.login(username="testcase", password="123test123") + url = reverse("polls:detail", args=(future_question.id,)) response = self.client.get(url) self.assertEqual(response.status_code, 404) @@ -212,6 +216,9 @@ class QuestionDetailViewTests(TestCase): past_question.pub_date = timezone.now() - timezone.timedelta(days=5) past_question.save() + user = User.objects.create_user(username="testcase", password="123test123") + self.client.login(username="testcase", password="123test123") + url = reverse("polls:detail", args=(past_question.id,)) response = self.client.get(url) self.assertContains(response, past_question.question_text) diff --git a/polls/urls.py b/polls/urls.py index 45ebddd..7ddb37f 100644 --- a/polls/urls.py +++ b/polls/urls.py @@ -8,4 +8,5 @@ urlpatterns = [ path("/", views.DetailView.as_view(), name="detail"), path("/results/", views.ResultsView.as_view(), name="results"), path("/vote/", views.vote, name="vote"), + path("signup/", views.SignUpView.as_view(), name="signup"), ] diff --git a/polls/views.py b/polls/views.py index 9abf5a9..ca199e0 100644 --- a/polls/views.py +++ b/polls/views.py @@ -1,11 +1,16 @@ from django.http import HttpResponseRedirect -from django.shortcuts import get_object_or_404, render +from django.shortcuts import get_object_or_404, render, redirect from django.urls import reverse from django.views import generic from django.utils import timezone -from django.views.generic import TemplateView +from django.urls import reverse_lazy, reverse +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.db.models import Q -from .models import Choice, Question +from .forms import SignUpForm +from .models import Choice, Question, Vote class IndexView(generic.ListView): @@ -15,13 +20,16 @@ class IndexView(generic.ListView): context_object_name = "latest_question_list" def get_queryset(self): - """Return the last five published questions.""" - return Question.objects.filter(pub_date__lte=timezone.now()).order_by( - "-pub_date" - )[:5] + """ + Return the last published questions that is published and haven't ended yet. + """ + now = timezone.now() + return Question.objects.filter( + Q(pub_date__lte=now) & (Q(end_date__gte=now) | Q(end_date=None)) + ).order_by("-pub_date") -class DetailView(generic.DetailView): +class DetailView(LoginRequiredMixin, generic.DetailView): """ Provide a view for detail page, a detail for each poll contain poll question and poll choices. @@ -48,43 +56,68 @@ class DetailView(generic.DetailView): context["end_date"] = question.end_date context["up_vote_count"] = question.up_vote_count context["down_vote_count"] = question.down_vote_count - context["participant_count"] = question.participant_count + + user = self.request.user + selected_choice = None + has_voted = False + + if user.is_authenticated: + try: + vote = question.vote_set.get(user=user) + selected_choice = vote.choice + has_voted = True + except Vote.DoesNotExist: + pass + + context["selected_choice"] = selected_choice + context["has_voted"] = has_voted return context -class ResultsView(generic.DetailView): +class ResultsView(LoginRequiredMixin, generic.DetailView): model = Question template_name = "polls/results.html" - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["question"] = self.object - return context - def render_to_response(self, context, **response_kwargs): return render(self.request, self.template_name, context) +class SignUpView(generic.CreateView): + form_class = SignUpForm + success_url = reverse_lazy('login') + template_name = 'registration/signup.html' + +@login_required def vote(request, question_id): """ - A function that update the database. Add vote count to choice that user vote - in specific question_id. + A function that updates the database. Adds a vote count to the choice that the user votes for + in a specific question_id. """ - question = get_object_or_404(Question, pk=question_id) try: selected_choice = question.choice_set.get(pk=request.POST["choice"]) except (KeyError, Choice.DoesNotExist): - return render( - request, - "polls/detail.html", - { - "question": question, - "error_message": "You didn't select a choice.", - }, - ) + messages.error(request, "You didn't select a choice.") + return render(request, "polls/detail.html", {"question": question}) + else: - selected_choice.votes += 1 - selected_choice.save() - return HttpResponseRedirect(reverse("polls:results", args=(question.id,))) + if question.can_vote(): + if request.method == "POST" and "vote-button" in request.POST: + if Vote.objects.filter(user=request.user, question=question).exists(): + old_vote = question.vote_set.get(user=request.user) + old_vote.choice = selected_choice + old_vote.save() + + messages.success(request, "You vote successfully🥳") + return HttpResponseRedirect(reverse("polls:results", args=(question.id,))) + else: + messages.success(request, "You vote successfully🥳") + Vote.objects.create(choice=selected_choice, user=request.user, question=question).save() + return HttpResponseRedirect(reverse("polls:results", args=(question.id,))) + else: + messages.error(request, "You cannot vote by typing the URL.") + return render(request, "polls/detail.html", {"question": question}) + else: + messages.error(request, "You can not vote on this question.") + return HttpResponseRedirect(reverse("polls:index")) \ No newline at end of file