diff --git a/data/polls-v1.json b/data/polls-v1.json index c120753..1c95e50 100644 --- a/data/polls-v1.json +++ b/data/polls-v1.json @@ -4,7 +4,13 @@ "pk": 1, "fields": { "question_text": "Python vs C++, which one is better in your opinion?", - "pub_date": "2023-08-28T13:38:42Z" + "short_description": "Cool kids have polls", + "long_description": "No description provide for this poll.", + "pub_date": "2023-08-28T13:38:42Z", + "end_date": "2025-09-24T22:56:52Z", + "up_vote_count": 0, + "down_vote_count": 0, + "participant_count": 0 } }, { @@ -12,7 +18,55 @@ "pk": 2, "fields": { "question_text": "The chicken and the egg, which came first?", - "pub_date": "2023-08-28T13:39:16Z" + "short_description": "Cool kids have polls", + "long_description": "No description provide for this poll.", + "pub_date": "2023-08-28T13:39:16Z", + "end_date": "2023-11-05T06:30:16Z", + "up_vote_count": 12, + "down_vote_count": 4, + "participant_count": 11 + } +}, +{ + "model": "polls.question", + "pk": 3, + "fields": { + "question_text": "Do you love Django?", + "short_description": "Cool kids have polls", + "long_description": "No description provide for this poll.", + "pub_date": "2023-11-07T15:40:31Z", + "end_date": "2023-09-20T13:41:33Z", + "up_vote_count": 3000, + "down_vote_count": 50, + "participant_count": 555 + } +}, +{ + "model": "polls.question", + "pk": 4, + "fields": { + "question_text": "So far so good?", + "short_description": "Cool kids have polls", + "long_description": "No description provide for this poll.", + "pub_date": "2023-09-09T06:30:50.690Z", + "end_date": "2023-09-30T12:54:30Z", + "up_vote_count": 0, + "down_vote_count": 0, + "participant_count": 234 + } +}, +{ + "model": "polls.question", + "pk": 5, + "fields": { + "question_text": "Do you love Django?", + "short_description": "Cool kids have polls", + "long_description": "No description provide for this poll.", + "pub_date": "2023-09-09T13:42:27.501Z", + "end_date": "2023-09-21T13:42:17Z", + "up_vote_count": 1, + "down_vote_count": 1, + "participant_count": 1 } }, { @@ -21,7 +75,7 @@ "fields": { "question": 2, "choice_text": "Chicken", - "votes": 5 + "votes": 55 } }, { @@ -30,7 +84,7 @@ "fields": { "question": 2, "choice_text": "Egg", - "votes": 2 + "votes": 56 } }, { @@ -39,7 +93,7 @@ "fields": { "question": 1, "choice_text": "Python!", - "votes": 2 + "votes": 6 } }, { @@ -48,6 +102,78 @@ "fields": { "question": 1, "choice_text": "C++", + "votes": 3 + } +}, +{ + "model": "polls.choice", + "pk": 8, + "fields": { + "question": 3, + "choice_text": "Yeah for sure!", + "votes": 0 + } +}, +{ + "model": "polls.choice", + "pk": 9, + "fields": { + "question": 3, + "choice_text": "nah", + "votes": 0 + } +}, +{ + "model": "polls.choice", + "pk": 13, + "fields": { + "question": 4, + "choice_text": "Yes!", + "votes": 5 + } +}, +{ + "model": "polls.choice", + "pk": 14, + "fields": { + "question": 4, + "choice_text": "No!", + "votes": 4 + } +}, +{ + "model": "polls.choice", + "pk": 15, + "fields": { + "question": 4, + "choice_text": "Okay!", + "votes": 5 + } +}, +{ + "model": "polls.choice", + "pk": 16, + "fields": { + "question": 4, + "choice_text": "Thank you!", + "votes": 2 + } +}, +{ + "model": "polls.choice", + "pk": 17, + "fields": { + "question": 5, + "choice_text": "Yeah for sure!", + "votes": 2 + } +}, +{ + "model": "polls.choice", + "pk": 18, + "fields": { + "question": 5, + "choice_text": "nah", "votes": 2 } } diff --git a/mysite/settings.py b/mysite/settings.py index 3906c9f..d4d41bc 100644 --- a/mysite/settings.py +++ b/mysite/settings.py @@ -26,10 +26,15 @@ 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='Asia/Bangkok', cast=str) +TIME_ZONE = config('TIME_ZONE', default='UTC', cast=str) +USE_I18N = True + +USE_TZ = True # Application definition INSTALLED_APPS = [ diff --git a/mysite/urls.py b/mysite/urls.py index b5d5113..1b67d3f 100644 --- a/mysite/urls.py +++ b/mysite/urls.py @@ -1,10 +1,10 @@ from django.contrib import admin from django.urls import include, path -from polls.views import HomeView +from django.views.generic import RedirectView urlpatterns = [ - path('', HomeView.as_view(), name='home'), + path('', RedirectView.as_view(pattern_name='polls:index'), name='home_redirect'), path("polls/", include("polls.urls")), path('admin/', admin.site.urls), ] diff --git a/polls/admin.py b/polls/admin.py index 0580846..2fc596f 100644 --- a/polls/admin.py +++ b/polls/admin.py @@ -12,6 +12,8 @@ class QuestionAdmin(admin.ModelAdmin): fieldsets = [ (None, {"fields": ["question_text"]}), ("End date", {"fields": ["end_date"], "classes": ["collapse"]}), + ("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"] inlines = [ChoiceInline] diff --git a/polls/migrations/0003_alter_choice_votes_alter_question_pub_date.py b/polls/migrations/0003_alter_choice_votes_alter_question_pub_date.py new file mode 100644 index 0000000..434fa51 --- /dev/null +++ b/polls/migrations/0003_alter_choice_votes_alter_question_pub_date.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.4 on 2023-09-09 05:27 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('polls', '0002_question_end_date_alter_question_pub_date'), + ] + + operations = [ + migrations.AlterField( + model_name='choice', + name='votes', + field=models.PositiveIntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(2147483647)]), + ), + migrations.AlterField( + model_name='question', + name='pub_date', + field=models.DateTimeField(auto_now_add=True, verbose_name='date published'), + ), + ] diff --git a/polls/migrations/0004_alter_question_pub_date.py b/polls/migrations/0004_alter_question_pub_date.py new file mode 100644 index 0000000..ed28b91 --- /dev/null +++ b/polls/migrations/0004_alter_question_pub_date.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.4 on 2023-09-09 08:15 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('polls', '0003_alter_choice_votes_alter_question_pub_date'), + ] + + operations = [ + migrations.AlterField( + model_name='question', + name='pub_date', + field=models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='date published'), + ), + ] diff --git a/polls/migrations/0005_question_long_description_question_short_description_and_more.py b/polls/migrations/0005_question_long_description_question_short_description_and_more.py new file mode 100644 index 0000000..35299d7 --- /dev/null +++ b/polls/migrations/0005_question_long_description_question_short_description_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.4 on 2023-09-09 08:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('polls', '0004_alter_question_pub_date'), + ] + + operations = [ + migrations.AddField( + model_name='question', + name='long_description', + field=models.TextField(default='No description provide for this poll.', max_length=2000), + ), + migrations.AddField( + model_name='question', + name='short_description', + field=models.CharField(default='Cool kids have polls', max_length=200), + ), + migrations.AlterField( + model_name='question', + name='question_text', + field=models.CharField(max_length=100), + ), + ] diff --git a/polls/migrations/0006_question_down_vote_count_question_up_vote_count.py b/polls/migrations/0006_question_down_vote_count_question_up_vote_count.py new file mode 100644 index 0000000..2b70532 --- /dev/null +++ b/polls/migrations/0006_question_down_vote_count_question_up_vote_count.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.4 on 2023-09-09 08:33 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('polls', '0005_question_long_description_question_short_description_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='question', + name='down_vote_count', + field=models.PositiveIntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(2147483647)]), + ), + migrations.AddField( + model_name='question', + name='up_vote_count', + field=models.PositiveIntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(2147483647)]), + ), + ] diff --git a/polls/migrations/0007_question_participant_count.py b/polls/migrations/0007_question_participant_count.py new file mode 100644 index 0000000..df81db9 --- /dev/null +++ b/polls/migrations/0007_question_participant_count.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.4 on 2023-09-09 08:36 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('polls', '0006_question_down_vote_count_question_up_vote_count'), + ] + + operations = [ + migrations.AddField( + model_name='question', + name='participant_count', + field=models.PositiveIntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(2147483647)]), + ), + ] diff --git a/polls/models.py b/polls/models.py index f765548..313d213 100644 --- a/polls/models.py +++ b/polls/models.py @@ -9,12 +9,11 @@ Attributes: None """ -import datetime - from django.db import models from django.utils import timezone from django.contrib import admin from django.core.validators import MaxValueValidator, MinValueValidator +from django.db.models import Sum class Question(models.Model): @@ -26,9 +25,14 @@ class Question(models.Model): pub_date (datetime): The date and time when the question was published. """ - question_text = models.CharField(max_length=200) - pub_date = models.DateTimeField("date published", auto_now_add=True) + 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) 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)]) def was_published_recently(self): """ @@ -38,7 +42,7 @@ class Question(models.Model): bool: True if the question was published within the last day, else False. """ now = timezone.now() - return now - datetime.timedelta(days=1) <= self.pub_date <= now + return now - timezone.timedelta(days=1) <= self.pub_date <= now @admin.display( boolean=True, @@ -47,7 +51,7 @@ class Question(models.Model): ) def was_published_recently(self): now = timezone.now() - return now - datetime.timedelta(days=1) <= self.pub_date <= now + return now - timezone.timedelta(days=1) <= self.pub_date <= now def __str__(self): """ @@ -78,6 +82,57 @@ class Question(models.Model): else: return self.pub_date <= now <= self.end_date + def calculate_time_left(self): + """ + Calculate the time left until the end date. + + Returns: + str: A formatted string representing the time left. + """ + if self.end_date is None: + return "No end date" + + now = timezone.now() + time_left = self.end_date - now + + days, seconds = divmod(time_left.total_seconds(), 86400) + hours, seconds = divmod(seconds, 3600) + minutes, seconds = divmod(seconds, 60) + + time_left_str = "" + if days > 0: + time_left_str += f"{int(days)} D " + elif hours > 0: + time_left_str += f"{int(hours)} H " + elif minutes > 0: + time_left_str += f"{int(minutes)} M " + elif seconds > 0: + time_left_str += f"{int(seconds)} S " + + return time_left_str.strip() + + @property + def time_left(self): + return self.calculate_time_left() + + def calculate_vote_percentage(self): + total_vote = self.up_vote_count + self.down_vote_count + if total_vote == 0: + return (0, 0) + + up_vote_percentage = self.up_vote_count / total_vote * 100 + down_vote_percentage = self.down_vote_count / total_vote * 100 + + return (int(up_vote_percentage), int(down_vote_percentage)) + + @property + def up_vote_percentage(self): + return self.calculate_vote_percentage()[0] + + @property + def down_vote_percentage(self): + return self.calculate_vote_percentage()[1] + class Choice(models.Model): """ @@ -93,6 +148,29 @@ class Choice(models.Model): 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): + 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) + def __str__(self): """ Returns a string representation of the choice. diff --git a/polls/static/polls/base.css b/polls/static/polls/base.css index afd26f9..3186c50 100644 --- a/polls/static/polls/base.css +++ b/polls/static/polls/base.css @@ -1,191 +1,9 @@ /*! NAVBAR */ -header { - background-color: #1C1C1C; - color: #fff; - padding: 20px; - text-align: center; - display: flex; - justify-content: center; - align-items: center; - box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1); - border-radius: 10px; -} - -.nav-container { - display: flex; - justify-content: center; - align-items: center; -} - -.nav-left h1 a { - text-decoration: none; - color: #fff; - font-size: 24px; -} - - -.nav-right ul { - list-style: none; - padding: 0; - margin: 0; - display: flex; - align-items: center; -} - -.nav-right li { - margin: 0 10px; -} - -.nav-right a { - text-decoration: none; - color: #fff; - font-weight: bold; - padding: 10px 20px; - border: 2px solid #fff; - border-radius: 5px; - transition: background-color 0.3s ease, color 0.3s ease; -} - -.nav-right a:hover { - background-color: #fff; - color: #007bff; -} - -/*! HOME AND POLL CARD */ - -.hero-section { - background-size: cover; - background-position: center; - text-align: center; - color: #1c1c1c; -} - -.hero-content { - max-width: 800px; - margin: 0 auto; -} - -h1 { - font-size: 36px; - margin-bottom: 20px; -} - -.polls-section { - background-size: cover; - background-position: center; - text-align: center; - color: #1c1c1c; -} - -.poll-cards { - display: flex; - flex-wrap: wrap; - gap: 20px; - justify-content: center; - margin-top: 30px; -} - -.poll-card { - background-color: #fff; - border: 1px solid #e0e0e0; - padding: 20px; - border-radius: 5px; - box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1); - transition: transform 0.3s ease; - cursor: pointer; -} - -.poll-card:hover { - transform: translateY(-5px); -} - -/*! DETAILED */ - -.poll-details { - padding: 30px; - background-color: #fff; - border-radius: 10px; - box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1); -} - -.poll-form { - text-align: center; -} - -.poll-question { - font-size: 24px; - margin-bottom: 20px; -} - -.error-message { - color: red; - margin-bottom: 10px; -} - -.choice { - display: flex; - align-items: center; - margin: 10px 0; -} - -.choice input[type="radio"] { - margin-right: 10px; -} - -.choice-text { - font-size: 18px; -} - -.vote-button { - background-color: #007bff; - color: #fff; - padding: 10px 20px; - border: none; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.3s ease, color 0.3s ease; -} - -.vote-button:hover { - background-color: #0056b3; -} - -/*! RESULT */ - -.poll-results { - text-align: center; - padding: 30px; - background-color: #fff; - border-radius: 10px; - box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1); -} - -.poll-question { - font-size: 24px; - margin-bottom: 20px; -} - -.choice-list { - list-style: none; - padding: 0; - margin: 20px 0; - text-align: left; -} - -.choice-item { - font-size: 18px; - margin: 10px 0; -} - -.vote-again { - display: inline-block; - margin-top: 20px; - text-decoration: none; - color: #007bff; - transition: color 0.3s ease; -} - -.vote-again:hover { - color: #0056b3; -} \ No newline at end of file +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 700; + src: local('Open Sans Extrabold'), local('OpenSans-Extrabold'), + url('https://fonts.googleapis.com/css2?family=Kanit&family=Open+Sans&family=Roboto:ital@1&display=swap') format('truetype'); + } \ No newline at end of file diff --git a/polls/templates/polls/base.html b/polls/templates/polls/base.html index ae54f87..7987a04 100644 --- a/polls/templates/polls/base.html +++ b/polls/templates/polls/base.html @@ -1,25 +1,17 @@ {% load static %} - -
-Explore and participate in our weird poll questions.
-Total number of polls: {{ total_open_polls }}
-No polls are available.
- {% endif %} -Published on: {{ question.pub_date|date:"F j, Y" }}
-No polls are available.
- {% endif %} +{{ question.short_description }}
+{{ question.short_description }}
+