diff --git a/mysite/settings.py b/mysite/settings.py index d4d41bc..572a80b 100644 --- a/mysite/settings.py +++ b/mysite/settings.py @@ -26,15 +26,8 @@ SECRET_KEY = config('SECRET_KEY', default='k2pd1p)zwe0qy0k25=sli+7+n^vd-0h*&6vga #! SECURITY WARNING: don't run with debug turned on in production! DEBUG = config('DEBUG', default=False, cast=bool) -LANGUAGE_CODE = 'en-us' - ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='*', cast=Csv()) -TIME_ZONE = config('TIME_ZONE', default='UTC', cast=str) - -USE_I18N = True - -USE_TZ = True # Application definition INSTALLED_APPS = [ @@ -113,7 +106,7 @@ AUTH_PASSWORD_VALIDATORS = [ LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' +TIME_ZONE = config('TIME_ZONE', default='UTC', cast=str) USE_I18N = True @@ -124,6 +117,9 @@ USE_TZ = True # https://docs.djangoproject.com/en/4.2/howto/static-files/ STATIC_URL = '/static/' +# ! On production -> Configure your web server to serve the files in STATIC_ROOT under the URL STATIC_URL +# * https://docs.djangoproject.com/en/4.2/howto/static-files/deployment/ +STATIC_ROOT = BASE_DIR / "assets" STATICFILES_DIRS = [BASE_DIR] # Default primary key field type diff --git a/polls/admin.py b/polls/admin.py index 2fc596f..ee1e244 100644 --- a/polls/admin.py +++ b/polls/admin.py @@ -11,6 +11,7 @@ class ChoiceInline(admin.TabularInline): class QuestionAdmin(admin.ModelAdmin): fieldsets = [ (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"]}), diff --git a/polls/migrations/0008_alter_question_pub_date.py b/polls/migrations/0008_alter_question_pub_date.py new file mode 100644 index 0000000..02205c6 --- /dev/null +++ b/polls/migrations/0008_alter_question_pub_date.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.4 on 2023-09-09 15:16 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('polls', '0007_question_participant_count'), + ] + + operations = [ + migrations.AlterField( + model_name='question', + name='pub_date', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='date published'), + ), + ] diff --git a/polls/models.py b/polls/models.py index 313d213..1e06a4e 100644 --- a/polls/models.py +++ b/polls/models.py @@ -23,16 +23,32 @@ class Question(models.Model): Attributes: question_text (str): The text of the poll question. pub_date (datetime): The date and time when the question was published. + end_date (datetime): The date and time when the question will end. + long_description (str): The long description of the poll question. + short_description (str): The short description of the poll question. + up_vote_count (int): The number of up votes the question has received. + down_vote_count (int): The number of down votes the question has received. + participant_count (int): The number of participants in the poll. """ 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=False) + 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 + ) 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)]) + 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): """ @@ -101,13 +117,13 @@ class Question(models.Model): time_left_str = "" if days > 0: - time_left_str += f"{int(days)} D " + time_left_str += f"{int(days)} Days " elif hours > 0: - time_left_str += f"{int(hours)} H " + time_left_str += f"{int(hours)} Hours " elif minutes > 0: - time_left_str += f"{int(minutes)} M " + time_left_str += f"{int(minutes)} Mins " elif seconds > 0: - time_left_str += f"{int(seconds)} S " + time_left_str += f"{int(seconds)} Sec " return time_left_str.strip() @@ -116,6 +132,7 @@ class Question(models.Model): return self.calculate_time_left() def calculate_vote_percentage(self): + """Calculate the percentage of up votes and down votes.""" total_vote = self.up_vote_count + self.down_vote_count if total_vote == 0: return (0, 0) @@ -146,25 +163,32 @@ 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)]) + 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) + 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' + return "w-0" ratio = self.votes / total_votes scaled_value = ratio * 48 - return f'w-{int(round(scaled_value))}' + return f"w-{int(round(scaled_value))}" def calculate_percentage(self): - total_votes_for_question = self.question.choice_set.aggregate(Sum('votes'))['votes__sum'] or 0 + """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 @@ -176,4 +200,3 @@ class Choice(models.Model): Returns a string representation of the choice. """ return self.choice_text - \ No newline at end of file diff --git a/polls/static/polls/js/detail.js b/polls/static/polls/js/detail.js new file mode 100644 index 0000000..86e7e5a --- /dev/null +++ b/polls/static/polls/js/detail.js @@ -0,0 +1,38 @@ +const toggleChoice = (button, choiceId) => { + const choiceInput = document.querySelector(`input[name="choice"][value="${choiceId}"]`); + + 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; + }); + + // 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').textContent = `You select: ${choiceText}`; + } + + // Enable the "Vote" button -> if select + const voteButton = document.getElementById('vote-button'); + voteButton.disabled = !document.querySelector('input[name="choice"]:checked'); + } +}; \ No newline at end of file diff --git a/polls/templates/polls/base.html b/polls/templates/polls/base.html index 7987a04..fc6124b 100644 --- a/polls/templates/polls/base.html +++ b/polls/templates/polls/base.html @@ -2,16 +2,20 @@ - - - - - - - Your Poll Website - - - {% block content %} - {% endblock content %} - - \ No newline at end of file + + + + + + + + + Your Poll Website + + + {% block content %} {% endblock content %} + + diff --git a/polls/templates/polls/detail.html b/polls/templates/polls/detail.html index 20fe9e2..6778f30 100644 --- a/polls/templates/polls/detail.html +++ b/polls/templates/polls/detail.html @@ -1,110 +1,102 @@ -{% extends 'polls/base.html' %} - -{% block content %} +{% extends 'polls/base.html' %} {% block content %}
- -
-

{{ question_text }}

-
-

{{ long_description }}

+ - -
-

Which one would you prefer:

+ +
+ +
+
+
+
+ πŸ‘€ {{ participant_count }} Participants + πŸ‘ {{ question.up_vote_percentage }}% Upvoted + πŸ‘Ž {{ question.down_vote_percentage }}% Downvoted +
+
+
+
+
+
-
-
- {% csrf_token %} -
-
Please Select a Choice😊
-
- {% if error_message %} -
-

{{ error_message }}

-
-
- - {% for choice in question.choice_set.all %} - - {% endfor %} -
-
- - -
- - Back to Polls - - - - -
- -
-
- + +
+
+
+
+

+ {{ long_description }} +

+
+
+
+ {% csrf_token %} +
+
Please Select a Choice😊
+
+ {% if error_message %} +
+

{{ error_message }}

+
+ {% endif %} +
+
+ + {% for choice in question.choice_set.all %} + + {% endfor %} +
+
+ + +
+ + Back to Polls + + + +
+
+
+
+
+
+
- {% endblock content %} diff --git a/polls/templates/polls/index.html b/polls/templates/polls/index.html index 94ca849..ff23cdc 100644 --- a/polls/templates/polls/index.html +++ b/polls/templates/polls/index.html @@ -1,97 +1,183 @@ -{% extends 'polls/base.html' %} - -{% block content %} +{% extends 'polls/base.html' %} {% block content %}
- - + + + +
+ {% comment %}
+
+
+
+
πŸ”Ž
+ +
+ +
+ +
+
{% endcomment %} + + + {% comment %}
+ +
{% endcomment %} + + +
+
+

Top Polls Today

+
+
+ {% for question in latest_question_list %} +
+ +
+

{{ question.question_text }}

+
+

{{ question.short_description }}

+
+ πŸ‘ + {{ question.up_vote_percentage }}% Upvoted + + πŸ‘Ž + {{ question.down_vote_percentage }}% Downvoted +
+ +
+ πŸ•’ {{ question.time_left }} + {{ question.participant_count }} Participants πŸ‘€ +
+
+ + +
+
+
+
+
+ {% endfor %} +
+
+
+ + +
+
+

All Polls

+
+
+ {% for question in latest_question_list %} +
+ +
+

{{ question.question_text }}

+
+

{{ question.short_description }}

+
+ πŸ‘ + {{ question.up_vote_percentage }}% Upvoted + + πŸ‘Ž + {{ question.down_vote_percentage }}% Downvoted +
+ +
+ πŸ•’ {{ question.time_left }} + {{ question.participant_count }} Participants πŸ‘€ +
+
+ + +
+
+ {% if forloop.counter|divisibleby:2 %} +
+
+ {% else %} +
+
+ {% endif %} +
+ {% endfor %} +
+
+
+
+
+
+ + Ku Poll + +

+ Ku Poll is a project in ISP. +

+ + + Repository + + + +
+
{% endblock content %} diff --git a/polls/templates/polls/results.html b/polls/templates/polls/results.html index 3415e92..2ecdf3a 100644 --- a/polls/templates/polls/results.html +++ b/polls/templates/polls/results.html @@ -2,116 +2,155 @@ {% block content %}
- -
-

{{ question.question_text }}

-
+ + + +
+ + +
+
+

Result Summary:

+ + {% for choice in question.choice_set.all %} +
+ {{ choice.choice_text }} +
+ πŸ‘ {{ choice.votes }} +
+
+
+
+ {% endfor %} +
+
+
+
- -
-

πŸ•΅οΈ Statistics

- {{ question.participant_count }} Participants πŸ‘€ - πŸ‘ {{ question.up_vote_percentage }}% - πŸ‘Ž {{ question.down_vote_percentage }}% + +
+
+

πŸ•΅οΈ Statistics

+ + {{ question.participant_count }} Participants πŸ‘€ + + πŸ‘ {{ question.up_vote_percentage }}% + πŸ‘Ž {{ question.down_vote_percentage }}%
+
+
- -
-

πŸ‘‹ Vote Percentage

-
- -
+ +
+
+

πŸ‘‹ Vote Percentage

+
+ +
+
+
- -
-

πŸ‘ Vote Count

-
- -
+ +
+
+

πŸ‘ Vote Count

+
+
-
- - - - Back to Polls - -
+
+
+
+ + + {% comment %} + Back to Polls + {% endcomment %} +
-{% endblock content %} \ No newline at end of file +{% endblock content %} diff --git a/polls/tests.py b/polls/tests.py index 187c2ab..c21dd77 100644 --- a/polls/tests.py +++ b/polls/tests.py @@ -49,7 +49,7 @@ class QuestionModelTests(TestCase): Questions with the default pub_date (now) are displayed on the index page. """ question = Question.objects.create(question_text="Default pub date question.") - + response = self.client.get(reverse("polls:index")) self.assertQuerySetEqual( response.context["latest_question_list"], @@ -215,4 +215,3 @@ class QuestionDetailViewTests(TestCase): url = reverse("polls:detail", args=(past_question.id,)) response = self.client.get(url) self.assertContains(response, past_question.question_text) - \ No newline at end of file diff --git a/polls/urls.py b/polls/urls.py index 6d2b663..45ebddd 100644 --- a/polls/urls.py +++ b/polls/urls.py @@ -9,4 +9,3 @@ urlpatterns = [ path("/results/", views.ResultsView.as_view(), name="results"), path("/vote/", views.vote, name="vote"), ] - \ No newline at end of file diff --git a/polls/views.py b/polls/views.py index e28c069..9abf5a9 100644 --- a/polls/views.py +++ b/polls/views.py @@ -16,7 +16,9 @@ class IndexView(generic.ListView): 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 Question.objects.filter(pub_date__lte=timezone.now()).order_by( + "-pub_date" + )[:5] class DetailView(generic.DetailView): @@ -36,18 +38,18 @@ class DetailView(generic.DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - + question = self.object - - context['question_text'] = question.question_text - context['short_description'] = question.short_description - context['long_description'] = question.long_description - context['pub_date'] = question.pub_date - 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 - + + context["question_text"] = question.question_text + context["short_description"] = question.short_description + context["long_description"] = question.long_description + context["pub_date"] = question.pub_date + 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 + return context @@ -57,7 +59,7 @@ class ResultsView(generic.DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['question'] = self.object + context["question"] = self.object return context def render_to_response(self, context, **response_kwargs): @@ -86,4 +88,3 @@ def vote(request, question_id): selected_choice.votes += 1 selected_choice.save() return HttpResponseRedirect(reverse("polls:results", args=(question.id,))) - \ No newline at end of file