Merge pull request #52 from Sosokker/iteration4 | Format Code, Add Search + Tag + Poll Creation

Iteration4 Format Code, Add Search + Tag + Poll Creation
This commit is contained in:
Sirin Puenggun 2023-09-18 02:25:26 +07:00 committed by GitHub
commit c9466b686d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 2399 additions and 696 deletions

12
.flake8 Normal file
View File

@ -0,0 +1,12 @@
[flake8]
max-line-length = 120
exclude=
migrations
mysite
logs
.venv
apps.py
setup.py
per-file-ignores =
.\polls\tests\test_sentiment_model.py: E501
.\polls\models.py: F811

97
Installation.md Normal file
View File

@ -0,0 +1,97 @@
## Installation
There are two ways to install and run this project.
1. Use `setup.py` or `setup.ps1`(for windows) to install the project.
2. Manually install the project with the Instruction in section 2.
### 1. Use `setup.py` or `setup.ps1`(for windows) to install the project.
1. Install [Python 3.11 or later](https://www.python.org/downloads/)
2. Clone this repository and Run `setup.py` to install and run the project
**Don't forget to answer the question from `setup.py` to setup the project**
This method will autogenerate Environment Variable for you.
```bash
git clone https://github.com/Sosokker/ku-polls
cd ku-polls
python setup.py
```
If you want to customize the environment variables, name of environment folder then run this command
```bash
python setup.py -custom
```
or run `setup.ps1` (For Windows User)
----
### 2. Manually install the project with this instruction.
1. Install [Python 3.11 or later](https://www.python.org/downloads/)
2. Run these commands to clone this repository and enter the directory.
```bash
git clone https://github.com/Sosokker/ku-polls
cd ku
```
3. (Optional: You can use venv instead)Install virtualenv via pip
```bash
python -m pip install --user virtualenv
```
4. Create virtual environment with `venv` or `virtualenv`.
```bash
python -m virtualenv .venv
or
python -m venv .venv
```
5. Use `virtual environment`
- Windows
```bash
.venv\Scripts\activate
```
- Linux or MacOS
```bash
source .venv/bin/activate
```
6. Install require module.
```
pip install -r requirements.txt
```
7. Create file call `.env` in `ku-polls` directory and add this line
**You can look at `sample.env` for more information and others environment variables to set.**
```bash
SECRET_KEY=your_secret_key
```
You can generate your own `your_secret_key` by this command
```bash
python manage.py shell -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
```
or
- [Django Secret Key Generator #1](https://djecrety.ir/)
- [Django Secret Key Generator #2](https://miniwebtool.com/django-secret-key-generator/)
**Don't forget to change `your_secret_key` to your secret key (without quote)**
8. Migrate database and load data into it then runserver.
```bash
python manage.py migrate
python manage.py loaddata data/users.json
python manage.py loaddata data/polls.json
python manage.py runserver
```
***NOTE***
By Default `DEBUG=False` and Django will not load Static files for you so if you want to apply CSS run this.
```bash
python manage.py runserver --insecure
```
or set `DEBUG=True`
or do the [collectstatic](https://docs.djangoproject.com/en/4.2/ref/contrib/staticfiles/)
Then connect to [http://127.0.0.1:8000/](http://127.0.0.1:8000/) or [localhost:8000/](localhost:8000/)

1265
data/alldata.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,180 +0,0 @@
[
{
"model": "polls.question",
"pk": 1,
"fields": {
"question_text": "Python vs C++, which one is better in your opinion?",
"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
}
},
{
"model": "polls.question",
"pk": 2,
"fields": {
"question_text": "The chicken and the egg, which came first?",
"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
}
},
{
"model": "polls.choice",
"pk": 4,
"fields": {
"question": 2,
"choice_text": "Chicken",
"votes": 55
}
},
{
"model": "polls.choice",
"pk": 5,
"fields": {
"question": 2,
"choice_text": "Egg",
"votes": 56
}
},
{
"model": "polls.choice",
"pk": 6,
"fields": {
"question": 1,
"choice_text": "Python!",
"votes": 6
}
},
{
"model": "polls.choice",
"pk": 7,
"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
}
}
]

View File

@ -9,7 +9,9 @@
"short_description": "🐍 vs 🇨 ++",
"long_description": "Let them fight.",
"trend_score": 60.0,
"tags": []
"tags": [
7
]
}
},
{
@ -22,7 +24,9 @@
"short_description": "Classic question.",
"long_description": "Most biologists state unequivocally that the egg came first. At their most basic level, eggs are just female sex cells. Hard external eggs that can be laid on land (also known as amniotic eggs) were a game changer for vertebrates. Anyway, let select you choice.",
"trend_score": 140.0,
"tags": []
"tags": [
4
]
}
},
{
@ -35,7 +39,9 @@
"short_description": "If you know, you know.",
"long_description": "The sentence where you will hear once every week.",
"trend_score": 120.0,
"tags": []
"tags": [
6
]
}
},
{
@ -48,7 +54,9 @@
"short_description": "Djago, I love it!",
"long_description": "Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design. Built by experienced developers, it takes care of much of the hassle of web development, so you can focus on writing your app without needing to reinvent the wheel. Its free and open source.",
"trend_score": 120.0,
"tags": []
"tags": [
7
]
}
},
{
@ -61,7 +69,9 @@
"short_description": "Swifty vs Ye Club",
"long_description": "It's no secret that Taylor Swift and Kanye West's relationship can be described as volatile at best. Let choose!",
"trend_score": 100.0,
"tags": []
"tags": [
8
]
}
},
{
@ -74,7 +84,9 @@
"short_description": "Please do my poll🥺",
"long_description": "My favorite code editor is VScode. What about you?",
"trend_score": 100.0,
"tags": []
"tags": [
7
]
}
},
{
@ -82,12 +94,15 @@
"pk": 8,
"fields": {
"question_text": "Which one is the best RIZZ line?",
"pub_date": "2023-09-14T15:52:03Z",
"end_date": "2023-11-14T17:00:00Z",
"pub_date": "2023-09-17T16:39:52Z",
"end_date": "2023-12-12T17:00:00Z",
"short_description": "RIZZ (noun) How good you are with flirting",
"long_description": "Rizz actually comes from the word charisma. It's a NYC Slang created by a twitch streamer named Kai cenat. This means that you have game, a good flirt line.",
"trend_score": 100.0,
"tags": []
"tags": [
5,
6
]
}
},
{
@ -100,7 +115,24 @@
"short_description": "Pizza without topping is not pizza!",
"long_description": "I mean, Yeah here are the choices ;)",
"trend_score": 100.0,
"tags": []
"tags": [
4
]
}
},
{
"model": "polls.question",
"pk": 14,
"fields": {
"question_text": "What is your favorite subjects?",
"pub_date": "2023-09-17T16:37:49Z",
"end_date": "2023-11-29T16:37:53Z",
"short_description": "Which subjects do you love?",
"long_description": "Here are the list of subjects, which one is the best for you!",
"trend_score": 100.0,
"tags": [
9
]
}
},
{
@ -196,7 +228,7 @@
"pk": 12,
"fields": {
"question": 6,
"choice_text": "Neither"
"choice_text": "Nobody is better than others."
}
},
{
@ -228,7 +260,7 @@
"pk": 16,
"fields": {
"question": 7,
"choice_text": "IntelliJ IDEA"
"choice_text": "JetBrains IDE"
}
},
{
@ -368,462 +400,245 @@
}
},
{
"model": "polls.vote",
"pk": 1,
"model": "polls.choice",
"pk": 38,
"fields": {
"choice": 1,
"user": 1,
"question": 1
"question": 9,
"choice_text": "Pepperoni"
}
},
{
"model": "polls.vote",
"pk": 2,
"model": "polls.choice",
"pk": 39,
"fields": {
"choice": 1,
"user": 3,
"question": 1
"question": 9,
"choice_text": "Chicken breast"
}
},
{
"model": "polls.vote",
"pk": 3,
"model": "polls.choice",
"pk": 40,
"fields": {
"choice": 2,
"user": 2,
"question": 1
"question": 9,
"choice_text": "Mushrooms"
}
},
{
"model": "polls.vote",
"pk": 4,
"model": "polls.choice",
"pk": 41,
"fields": {
"choice": 7,
"user": 2,
"question": 4
"question": 9,
"choice_text": "Tomato"
}
},
{
"model": "polls.vote",
"pk": 5,
"model": "polls.choice",
"pk": 42,
"fields": {
"choice": 6,
"user": 2,
"question": 3
"question": 9,
"choice_text": "Onions"
}
},
{
"model": "polls.vote",
"pk": 6,
"model": "polls.choice",
"pk": 43,
"fields": {
"choice": 3,
"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
}
},
{
"model": "polls.vote",
"pk": 13,
"fields": {
"choice": 6,
"user": 6,
"question": 3
}
},
{
"model": "polls.vote",
"pk": 14,
"fields": {
"choice": 7,
"user": 3,
"question": 4
}
},
{
"model": "polls.vote",
"pk": 15,
"fields": {
"choice": 2,
"user": 6,
"question": 1
}
},
{
"model": "polls.vote",
"pk": 16,
"fields": {
"choice": 9,
"user": 1,
"question": 4
}
},
{
"model": "polls.vote",
"pk": 17,
"fields": {
"choice": 6,
"user": 1,
"question": 3
}
},
{
"model": "polls.vote",
"pk": 18,
"fields": {
"choice": 25,
"user": 2,
"question": 8
}
},
{
"model": "polls.vote",
"pk": 19,
"fields": {
"choice": 15,
"user": 2,
"question": 7
}
},
{
"model": "polls.vote",
"pk": 20,
"fields": {
"choice": 24,
"user": 4,
"question": 8
}
},
{
"model": "polls.vote",
"pk": 21,
"fields": {
"choice": 25,
"user": 3,
"question": 8
}
},
{
"model": "polls.vote",
"pk": 22,
"fields": {
"choice": 14,
"user": 3,
"question": 6
}
},
{
"model": "polls.vote",
"pk": 23,
"fields": {
"choice": 28,
"user": 3,
"question": 9
}
},
{
"model": "polls.vote",
"pk": 24,
"fields": {
"choice": 27,
"user": 6,
"question": 8
}
},
{
"model": "polls.vote",
"pk": 25,
"fields": {
"choice": 15,
"user": 6,
"question": 7
}
},
{
"model": "polls.vote",
"pk": 26,
"fields": {
"choice": 29,
"user": 6,
"question": 9
}
},
{
"model": "polls.vote",
"pk": 27,
"fields": {
"choice": 26,
"user": 1,
"question": 8
}
},
{
"model": "polls.sentimentvote",
"pk": 1,
"fields": {
"user": 1,
"question": 1,
"vote_types": false
}
},
{
"model": "polls.sentimentvote",
"pk": 2,
"fields": {
"user": 2,
"question": 2,
"vote_types": true
}
},
{
"model": "polls.sentimentvote",
"pk": 3,
"fields": {
"user": 2,
"question": 4,
"vote_types": true
}
},
{
"model": "polls.sentimentvote",
"pk": 4,
"fields": {
"user": 2,
"question": 3,
"vote_types": false
}
},
{
"model": "polls.sentimentvote",
"pk": 5,
"fields": {
"user": 2,
"question": 1,
"vote_types": false
}
},
{
"model": "polls.sentimentvote",
"pk": 6,
"fields": {
"user": 6,
"question": 1,
"vote_types": true
}
},
{
"model": "polls.sentimentvote",
"pk": 7,
"fields": {
"user": 6,
"question": 3,
"vote_types": true
}
},
{
"model": "polls.sentimentvote",
"pk": 8,
"fields": {
"user": 6,
"question": 4,
"vote_types": true
}
},
{
"model": "polls.sentimentvote",
"pk": 9,
"fields": {
"user": 3,
"question": 1,
"vote_types": false
}
},
{
"model": "polls.sentimentvote",
"pk": 10,
"fields": {
"user": 3,
"question": 3,
"vote_types": true
}
},
{
"model": "polls.sentimentvote",
"pk": 11,
"fields": {
"user": 3,
"question": 4,
"vote_types": false
}
},
{
"model": "polls.sentimentvote",
"pk": 12,
"fields": {
"user": 3,
"question": 2,
"vote_types": true
}
},
{
"model": "polls.sentimentvote",
"pk": 14,
"fields": {
"user": 1,
"question": 4,
"vote_types": false
}
},
{
"model": "polls.sentimentvote",
"pk": 15,
"fields": {
"user": 1,
"question": 3,
"vote_types": true
}
},
{
"model": "polls.sentimentvote",
"pk": 16,
"fields": {
"user": 1,
"question": 2,
"vote_types": false
}
},
{
"model": "polls.sentimentvote",
"pk": 17,
"fields": {
"user": 2,
"question": 8,
"vote_types": true
}
},
{
"model": "polls.sentimentvote",
"pk": 18,
"fields": {
"user": 2,
"question": 7,
"vote_types": true
"choice_text": "Sublime Text"
}
},
{
"model": "polls.sentimentvote",
"pk": 19,
"model": "polls.choice",
"pk": 44,
"fields": {
"user": 4,
"question": 8,
"vote_types": true
"question": 7,
"choice_text": "Notepad++"
}
},
{
"model": "polls.sentimentvote",
"pk": 20,
"model": "polls.choice",
"pk": 45,
"fields": {
"user": 3,
"question": 8,
"vote_types": true
"question": 7,
"choice_text": "Atom"
}
},
{
"model": "polls.sentimentvote",
"pk": 21,
"model": "polls.choice",
"pk": 46,
"fields": {
"user": 3,
"question": 6,
"vote_types": false
"question": 7,
"choice_text": "Neovim"
}
},
{
"model": "polls.sentimentvote",
"pk": 22,
"model": "polls.choice",
"pk": 47,
"fields": {
"question": 7,
"choice_text": "Notepad"
}
},
{
"model": "polls.choice",
"pk": 48,
"fields": {
"question": 7,
"choice_text": "Visual Studio"
}
},
{
"model": "polls.choice",
"pk": 49,
"fields": {
"question": 7,
"choice_text": "Powerpoint"
}
},
{
"model": "polls.choice",
"pk": 50,
"fields": {
"question": 3,
"choice_text": "Maybe"
}
},
{
"model": "polls.choice",
"pk": 51,
"fields": {
"question": 14,
"choice_text": "Mathematics"
}
},
{
"model": "polls.choice",
"pk": 52,
"fields": {
"question": 14,
"choice_text": "Geography"
}
},
{
"model": "polls.choice",
"pk": 53,
"fields": {
"question": 14,
"choice_text": "Economics"
}
},
{
"model": "polls.choice",
"pk": 54,
"fields": {
"question": 14,
"choice_text": "Physics"
}
},
{
"model": "polls.choice",
"pk": 55,
"fields": {
"question": 14,
"choice_text": "History"
}
},
{
"model": "polls.choice",
"pk": 56,
"fields": {
"question": 14,
"choice_text": "Biology"
}
},
{
"model": "polls.choice",
"pk": 57,
"fields": {
"question": 14,
"choice_text": "Chemistry"
}
},
{
"model": "polls.choice",
"pk": 58,
"fields": {
"question": 14,
"choice_text": "Psychology"
}
},
{
"model": "polls.choice",
"pk": 59,
"fields": {
"question": 14,
"choice_text": "Philosophy"
}
},
{
"model": "polls.choice",
"pk": 60,
"fields": {
"question": 14,
"choice_text": "Other."
}
},
{
"model": "polls.choice",
"pk": 61,
"fields": {
"user": 3,
"question": 9,
"vote_types": false
"choice_text": "Other"
}
},
{
"model": "polls.sentimentvote",
"pk": 23,
"model": "polls.choice",
"pk": 62,
"fields": {
"user": 6,
"question": 8,
"vote_types": true
"question": 3,
"choice_text": "No Opinion."
}
},
{
"model": "polls.sentimentvote",
"pk": 24,
"model": "polls.tag",
"pk": 4,
"fields": {
"user": 6,
"question": 9,
"vote_types": false
"tag_text": "Food"
}
},
{
"model": "polls.sentimentvote",
"pk": 25,
"model": "polls.tag",
"pk": 5,
"fields": {
"user": 1,
"question": 8,
"vote_types": true
"tag_text": "Flirt"
}
},
{
"model": "polls.tag",
"pk": 6,
"fields": {
"tag_text": "Meme"
}
},
{
"model": "polls.tag",
"pk": 7,
"fields": {
"tag_text": "Programming"
}
},
{
"model": "polls.tag",
"pk": 8,
"fields": {
"tag_text": "Singer"
}
},
{
"model": "polls.tag",
"pk": 9,
"fields": {
"tag_text": "Education"
}
}
]

View File

@ -4,7 +4,7 @@
"pk": 1,
"fields": {
"password": "pbkdf2_sha256$600000$aDh9a1PXxcXAb8z3YIjAPX$NVH24kt/wMad+0fZcCii738dfojI4vL2ffXOwNRuLz4=",
"last_login": "2023-09-15T17:09:43.310Z",
"last_login": "2023-09-17T16:33:12.136Z",
"is_superuser": true,
"username": "admin",
"first_name": "",
@ -22,7 +22,7 @@
"pk": 2,
"fields": {
"password": "pbkdf2_sha256$600000$quZKLKT8Ec3TQgpdqlCkpX$o+VOOnRDLGf64qjHb239Yvsre74tPkC8hw1qH1un/hk=",
"last_login": "2023-09-15T17:07:42.782Z",
"last_login": "2023-09-17T16:07:12.447Z",
"is_superuser": false,
"username": "tester1",
"first_name": "",
@ -109,18 +109,18 @@
},
{
"model": "auth.user",
"pk": 7,
"pk": 8,
"fields": {
"password": "1234",
"last_login": null,
"password": "pbkdf2_sha256$600000$QL1ktlWKsyyG2IASDFrJTQ$L7vKabykHi4ir3IcCf3qE9tj/TrAyyfQMekQ4xj+s7I=",
"last_login": "2023-09-15T17:23:08.479Z",
"is_superuser": false,
"username": "test",
"username": "tester5",
"first_name": "",
"last_name": "",
"email": "",
"is_staff": false,
"is_active": true,
"date_joined": "2023-09-15T11:53:22.035Z",
"date_joined": "2023-09-15T17:23:07.965Z",
"groups": [],
"user_permissions": []
}

263
data/vote.json Normal file
View File

@ -0,0 +1,263 @@
[
{
"model": "polls.vote",
"pk": 1,
"fields": {
"choice": 1,
"user": 1,
"question": 1
}
},
{
"model": "polls.vote",
"pk": 2,
"fields": {
"choice": 1,
"user": 3,
"question": 1
}
},
{
"model": "polls.vote",
"pk": 3,
"fields": {
"choice": 2,
"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": 3,
"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
}
},
{
"model": "polls.vote",
"pk": 13,
"fields": {
"choice": 6,
"user": 6,
"question": 3
}
},
{
"model": "polls.vote",
"pk": 14,
"fields": {
"choice": 7,
"user": 3,
"question": 4
}
},
{
"model": "polls.vote",
"pk": 15,
"fields": {
"choice": 2,
"user": 6,
"question": 1
}
},
{
"model": "polls.vote",
"pk": 16,
"fields": {
"choice": 9,
"user": 1,
"question": 4
}
},
{
"model": "polls.vote",
"pk": 17,
"fields": {
"choice": 6,
"user": 1,
"question": 3
}
},
{
"model": "polls.vote",
"pk": 18,
"fields": {
"choice": 25,
"user": 2,
"question": 8
}
},
{
"model": "polls.vote",
"pk": 19,
"fields": {
"choice": 15,
"user": 2,
"question": 7
}
},
{
"model": "polls.vote",
"pk": 20,
"fields": {
"choice": 24,
"user": 4,
"question": 8
}
},
{
"model": "polls.vote",
"pk": 21,
"fields": {
"choice": 25,
"user": 3,
"question": 8
}
},
{
"model": "polls.vote",
"pk": 22,
"fields": {
"choice": 14,
"user": 3,
"question": 6
}
},
{
"model": "polls.vote",
"pk": 23,
"fields": {
"choice": 28,
"user": 3,
"question": 9
}
},
{
"model": "polls.vote",
"pk": 24,
"fields": {
"choice": 27,
"user": 6,
"question": 8
}
},
{
"model": "polls.vote",
"pk": 25,
"fields": {
"choice": 15,
"user": 6,
"question": 7
}
},
{
"model": "polls.vote",
"pk": 26,
"fields": {
"choice": 29,
"user": 6,
"question": 9
}
},
{
"model": "polls.vote",
"pk": 27,
"fields": {
"choice": 26,
"user": 1,
"question": 8
}
},
{
"model": "polls.vote",
"pk": 28,
"fields": {
"choice": 28,
"user": 1,
"question": 9
}
},
{
"model": "polls.vote",
"pk": 29,
"fields": {
"choice": 12,
"user": 1,
"question": 6
}
}
]

View File

@ -1,6 +1,6 @@
from django.contrib import admin
from .models import Choice, Question
from .models import Choice, Question, Tag
class ChoiceInline(admin.TabularInline):
@ -15,12 +15,15 @@ class QuestionAdmin(admin.ModelAdmin):
("End date", {"fields": ["end_date"], "classes": ["collapse"]}),
("Short Description", {"fields": ["short_description"], "classes": ["collapse"]}),
("Long Description", {"fields": ["long_description"], "classes": ["collapse"]}),
("Add Tag", {"fields": ["tags"], "classes": ["collapse"]})
]
list_display = ["question_text", "pub_date", "end_date", "was_published_recently", "can_vote", "trending_score"]
list_display = ["question_text", "pub_date", "end_date", "was_published_recently", "can_vote",
"trending_score", "get_tags"]
inlines = [ChoiceInline]
list_filter = ["pub_date", "end_date"]
search_fields = ["question_text"]
# https://stackoverflow.com/questions/10904848/adding-inline-many-to-many-objects-in-django-admin
admin.site.register(Question, QuestionAdmin)
admin.site.register(Tag) # Add Field to modify tags objects in Question

View File

@ -4,6 +4,7 @@ from django.apps import AppConfig
class PollsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'polls'
def ready(self) -> None:
import polls.signals
import polls.signals

View File

@ -2,22 +2,25 @@ import logging
from typing import Any
from django import forms
from django.apps import apps
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from .models import Question
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"
tailwind_class = """w-full border-2 border-gray-300 bg-gray-100 rounded-lg
focus:ring focus:border-blue-300 focus:shadow-none"""
logger = logging.getLogger('signup_form')
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.',
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.',}
error_messages={'min_length': 'Password must contain at least 8 characters.', }
)
password2 = forms.CharField(widget=forms.PasswordInput(attrs={'class': tailwind_class}),)
@ -37,4 +40,50 @@ class SignUpForm(UserCreationForm):
error_messages = {
'password_mismatch': "The two password fields didn't match.",
}
}
class PollSearchForm(forms.Form):
q = forms.CharField()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class PollCreateForm(forms.ModelForm):
box_style = """w-full py-2 px-2 border-2 border-gray-300 bg-gray-100 rounded-lg
focus:ring focus:border-blue-300 focus:shadow-none"""
large_box_style = """w-full border-2 border-gray-300 bg-gray-100 rounded-lg
focus:ring focus:border-blue-300 focus:shadow-none"""
question_text = forms.CharField(min_length=10, max_length=100, required=True,
widget=forms.TextInput(attrs={'class': box_style,
'placeholder': "What is your question?"}))
pub_date = forms.DateTimeField(widget=forms.DateInput(attrs={'type': 'date'}))
end_date = forms.DateTimeField(widget=forms.DateInput(attrs={'type': 'date'}))
short_description = forms.CharField(max_length=200,
widget=forms.TextInput(
attrs={'class': box_style,
'placeholder': "Short description (Maximum 200 characters)"}))
long_description = forms.CharField(max_length=2000,
widget=forms.Textarea(
attrs={'class': large_box_style,
'placeholder': "Long description (Maximum 2000 characters)"}))
user_choice = forms.CharField(
widget=forms.TextInput(attrs={'placeholder': 'Enter a choice'}),
required=True
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Tag = apps.get_model('polls', 'Tag')
tags = forms.MultipleChoiceField(
choices=[(tag.id, tag.tag_text) for tag in Tag.objects.all()],
widget=forms.CheckboxSelectMultiple,
)
class Meta:
model = Question
fields = ['question_text', 'pub_date', 'end_date', 'short_description', 'long_description', 'tags']

View File

@ -12,7 +12,6 @@ Attributes:
from django.db import models, IntegrityError
from django.utils import timezone
from django.contrib import admin
from django.db.models import Sum
from django.contrib.auth.models import User
@ -23,7 +22,7 @@ class Tag(models.Model):
tag_text = models.CharField(max_length=50)
def __str__(self):
return self.name
return self.tag_text
class Question(models.Model):
@ -128,6 +127,7 @@ class Question(models.Model):
@property
def time_left(self):
"""Return time till ending of the poll"""
return self.calculate_time_left()
def calculate_vote_percentage(self):
@ -143,10 +143,12 @@ class Question(models.Model):
@property
def up_vote_percentage(self):
"""Retrieve up vote percentage from calculate_vote_percentage"""
return self.calculate_vote_percentage()[0]
@property
def down_vote_percentage(self):
"""Retrieve down vote percentage from calculate_vote_percentage"""
return self.calculate_vote_percentage()[1]
@property
@ -166,7 +168,7 @@ class Question(models.Model):
self.save()
except IntegrityError:
vote = self.sentimentvote_set.filter(user=user)
if vote[0].vote_types == False:
if vote[0].vote_types is False:
vote.update(vote_types=True)
self.save()
else:
@ -182,7 +184,7 @@ class Question(models.Model):
self.save()
except IntegrityError:
vote = self.sentimentvote_set.filter(user=user)
if vote[0].vote_types == True:
if vote[0].vote_types is True:
vote.update(vote_types=False)
self.save()
else:
@ -191,10 +193,12 @@ class Question(models.Model):
@property
def up_vote_count(self):
"""Count up vote of Question"""
return self.sentimentvote_set.filter(question=self, vote_types=True).count()
@property
def down_vote_count(self):
"""Count down vote of Question"""
return self.sentimentvote_set.filter(question=self, vote_types=False).count()
def trending_score(self, up=None, down=None):
@ -202,8 +206,8 @@ class Question(models.Model):
published_date_duration = timezone.now() - self.pub_date
score = 0
if (published_date_duration.seconds < 259200): # Second unit
score += 100
if (published_date_duration.seconds < 259200): # Second unit
score += 100
elif (published_date_duration.seconds < 604800):
score += 75
elif (published_date_duration.seconds < 2592000):
@ -211,17 +215,20 @@ class Question(models.Model):
else:
score += 25
if (up == None) and (down == None):
if (up is None) and (down is None):
score += ((self.up_vote_count/5) - (self.down_vote_count/5)) * 100
else:
score += ((up/5) - (down/5)) * 100
return score
def get_tags(self, *args, **kwargs):
return "-".join([tag.tag_text for tag in self.tags.all()])
def save(self, *args, **kwargs):
"""Modify save method of Question object"""
# to-be-added instance # * https://github.com/django/django/blob/866122690dbe233c054d06f6afbc2f3cc6aea2f2/django/db/models/base.py#L447
# to-be-added instance
# * https://github.com/django/django/blob/866122690dbe233c054d06f6afbc2f3cc6aea2f2/django/db/models/base.py#L447
if self._state.adding:
try:
self.trend_score = self.trending_score()
@ -263,7 +270,7 @@ class Vote(models.Model):
def __str__(self):
return f"{self.user} voted for {self.choice} in {self.question}"
# ! Most of the code from https://stackoverflow.com/a/70869267
class SentimentVote(models.Model):
@ -288,4 +295,4 @@ class SentimentVote(models.Model):
unique_together (list of str): Ensures that a user can only cast one sentiment vote (upvote or downvote)
for a specific question.
"""
unique_together = ['user', 'question']
unique_together = ['user', 'question']

View File

@ -4,6 +4,7 @@ from django.dispatch import receiver
log = logging.getLogger("django")
def get_client_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
@ -12,9 +13,10 @@ def get_client_ip(request):
ip = request.META.get('REMOTE_ADDR')
return ip
#! https://stackoverflow.com/questions/37618473/how-can-i-log-both-successful-and-failed-login-and-logout-attempts-in-django
# !https://stackoverflow.com/questions/37618473/how-can-i-log-both-successful-and-failed-login-and-logout-attempts-in-django
@receiver(user_logged_in)
def user_logged_in_callback(sender, request, user, **kwargs):
def user_logged_in_callback(sender, request, user, **kwargs):
ip = get_client_ip(request)
log.info('Login User: {user} via ip: {ip}'.format(
@ -22,8 +24,9 @@ def user_logged_in_callback(sender, request, user, **kwargs):
ip=ip
))
@receiver(user_logged_out)
def user_logged_out_callback(sender, request, user, **kwargs):
def user_logged_out_callback(sender, request, user, **kwargs):
ip = get_client_ip(request)
log.info('Logout User: {user} via ip: {ip}'.format(
@ -31,8 +34,9 @@ def user_logged_out_callback(sender, request, user, **kwargs):
ip=ip
))
@receiver(user_login_failed)
def user_login_failed_callback(sender, credentials, **kwargs):
log.warning('Login Failed for: {credentials}'.format(
credentials=credentials,
))
))

View File

@ -0,0 +1,145 @@
{% extends "polls/base.html" %}
{% block content %}
<header>
<link href="https://cdnjs.cloudflare.com/ajax/libs/flowbite/1.8.1/flowbite.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/flowbite/1.8.1/flowbite.min.js"></script>
</header>
<div class="min-h-screen bg-gray-100 flex items-center justify-center p-8">
<div class="bg-white p-8 shadow-md rounded-md w-full border-solid border-neutral-500 border-2">
<a href="{% url 'polls:index' %}" class="block text-blue-500 hover:underline mb-4">&lt; Back</a>
<form method="post" class="space-y-4">
{% csrf_token %}
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="{{ form.question_text.id_for_label }}">Question</label>
{{ form.question_text }}
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="{{ form.pub_date.id_for_label }}">Publication Date</label>
{{ form.pub_date }}
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="{{ form.end_date.id_for_label }}">End Date</label>
{{ form.end_date }}
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="{{ form.short_description.id_for_label }}">Short Description</label>
{{ form.short_description }}
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="{{ form.long_description.id_for_label }}">Long Description</label>
{{ form.long_description }}
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">Tags(Press Ctrl to select Multiple Tags)</label>
{{ form.tags }}
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">Choice(Seperate by comma)</label>
{{ form.user_choice }}
</div>
{% comment %} <div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="{{ form.user_choice.id_for_label }}">User Choice</label>
<div class="flex space-x-2">
<input type="text" id="{{ form.user_choice.id_for_label }}" class="form-input" placeholder="{{ form.user_choice.field.widget.attrs.placeholder }}">
<button type="button" id="add-choice-button" class="bg-blue-500 text-white font-bold py-2 px-4 rounded-full hover:bg-blue-700 focus:outline-none focus:shadow-outline-blue active:bg-blue-800">Add</button>
</div>
</div>
<div id="user-choices-container" class="mb-4 pb-2">
</div> {% endcomment %}
<hr class="h-px my-4 bg-neutral-0 border-0 dark:bg-neutral-0" />
<!--Create Button-->
<a data-modal-target="popup-modal" data-modal-toggle="popup-modal" class="bg-blue-500 text-white font-bold py-2 mt-3 px-4 rounded-full hover:bg-blue-700 focus:outline-none focus:shadow-outline-blue active:bg-blue-800 cursor-pointer">Create Poll</a>
<!--Code from https://flowbite.com/docs/components/modal/-->
<div id="popup-modal" tabindex="-1" class="fixed top-0 left-0 right-0 z-50 hidden p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] max-h-full">
<div class="relative w-full max-w-md max-h-full">
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
<button type="button" class="absolute top-3 right-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ml-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white" data-modal-hide="popup-modal">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
</svg>
<span class="sr-only">Close modal</span>
</button>
<div class="p-6 text-center">
<svg class="mx-auto mb-4 text-gray-400 w-12 h-12 dark:text-gray-200" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 11V6m0 8h.01M19 10a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
</svg>
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">Are you sure to create this poll?</h3>
<button data-modal-hide="popup-modal" type="submit" class="text-white bg-green-500 hover:bg-green-800 focus:ring-4 focus:outline-none focus:ring-green-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center mr-2">
Yes, I'm sure
</button>
<button data-modal-hide="popup-modal" type="button" class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">No, cancel</button>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
<!--Thank you CHATGPT-->
<!--Thank you CHATGPT-->
<!--Thank you CHATGPT-->
{% comment %} <script>
const userChoicesContainer = document.getElementById('user-choices-container');
const userInput = document.getElementById('{{ form.user_choice.id_for_label }}');
const addButton = document.getElementById('add-choice-button');
// Function to create a new choice element
function createChoiceElement(choiceValue) {
const choiceElement = document.createElement('div');
choiceElement.textContent = choiceValue;
choiceElement.className = 'bg-gray-300 p-2 rounded-md';
// Add a delete button next to the choice
const deleteButton = document.createElement('button');
deleteButton.textContent = 'Delete';
deleteButton.className = 'bg-red-500 text-white font-bold py-1 px-2 mx-4 rounded-full hover:bg-red-700 focus:outline-none focus:shadow-outline-red active:bg-red-800';
deleteButton.addEventListener('click', function () {
// Remove the choice element when the delete button is clicked
userChoicesContainer.removeChild(choiceElement);
});
choiceElement.appendChild(deleteButton);
return choiceElement;
}
addButton.addEventListener('click', function () {
const choiceValue = userInput.value.trim();
if (choiceValue !== '') {
// Create a new choice element
const choiceElement = createChoiceElement(choiceValue);
// Append the choice element to the container
userChoicesContainer.appendChild(choiceElement);
// Clear the user input
userInput.value = '';
}
});
// Handle delete button clicks using event delegation
userChoicesContainer.addEventListener('click', function (event) {
if (event.target && event.target.tagName === 'BUTTON' && event.target.textContent === 'Delete') {
// Remove the choice element when a delete button is clicked
userChoicesContainer.removeChild(event.target.parentNode);
}
});
</script> {% endcomment %}
{% endblock content %}

View File

@ -12,14 +12,11 @@
</div>
<!-- Button -->
<div>
{% comment %}
<button class="mr-4 rounded-md bg-green-500 px-4 py-2 text-white">New Poll</button>
{% endcomment %}
<div class="flex flex-wrap items-center space-x-2">
<header class="flex items-center justify-center">
<div class="flex flex-wrap items-center space-x-2">
<!--Searchhhh-->
<form class="group relative z-30 flex flex-grow flex-col items-center space-y-2 rounded-lg border-2 border-neutral-700 bg-white p-1 pl-2 text-lg md:flex-row md:space-y-0 md:space-x-1 md:rounded-full">
<form action="{% url "polls:search_poll" %}" method="get" class="group relative z-30 flex flex-grow flex-col items-center space-y-2 rounded-lg border-2 border-neutral-700 bg-white p-1 pl-2 text-lg md:flex-row md:space-y-0 md:space-x-1 md:rounded-full">
<div class="flex w-full items-center rounded-lg bg-neutral-200 py-1 px-2 focus-within:outline-none focus-within:ring-2 focus-within:ring-purple-400 md:rounded-full">
<div>🔎</div>
<input type="text" name="q" class="font-semibold w-full border-none bg-transparent focus:outline-none focus:ring focus:ring-transparent text-sm">
@ -34,9 +31,9 @@
</div>
</header>
{% if user.is_authenticated %}
<button class="flex items-center whitespace-nowrap rounded-full border border-transparent bg-green-500 px-5 py-2 text-sm font-bold text-white transition duration-150 ease-in-out hover:scale-[101%] hover:bg-green-700 focus:bg-green-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 active:bg-green-900">
<a href="{% url "polls:create_poll" %}" class="flex items-center whitespace-nowrap rounded-full border border-transparent bg-green-500 px-5 py-2 text-sm font-bold text-white transition duration-150 ease-in-out hover:scale-[101%] hover:bg-green-700 focus:bg-green-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 active:bg-green-900">
New Poll
</button>
</a>
<a href="{% url 'logout' %}"
class="flex items-center whitespace-nowrap rounded-full border border-transparent bg-red-600 px-5 py-2 text-sm font-bold text-white transition duration-150 ease-in-out hover:scale-[101%] hover:bg-red-700 focus:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 active:bg-neutral-900">
<span>Sign out <span class="hidden sm:inline-block">😭</span></span>
@ -90,6 +87,7 @@
<h2 class="mb-2 text-xl font-bold truncate">{{ question.question_text }}</h2>
<hr class="h-px my-2 bg-gray-200 border-0 dark:bg-gray-400" />
<p class="mb-2 text-gray-600">{{ question.short_description }}</p>
<!--Up, Down Vote-->
<div class="mb-2 flex items-center text-gray-600">
<span class="mr-2">👍</span>
<span>{{ question.up_vote_percentage }}% Upvoted</span>
@ -97,12 +95,20 @@
<span class="ml-4 mr-2">👎</span>
<span>{{ question.down_vote_percentage }}% Downvoted</span>
</div>
<!-- Tag / Time -->
<!-- Participant, Time -->
<div class="flex items-center text-gray-600">
<span class="mr-2 rounded-md bg-green-500 px-2 py-1 text-white">🕒 {{ question.time_left }}</span>
<span class="mr-2 rounded-md bg-orange-100 px-2 py-1 text-black">{{ question.participants }} Participants 👤</span>
</div>
<div class="flex items-center text-gray-600 py-4">
<!-- Tags-->
<div class="flex items-center text-gray-600 pt-2">
{% for tag in question.tags.all %}
<span class="mr-2 rounded-md bg-blue-100 px-1 py-1 text-blue-400 text-xs text-black font-bold">{{ tag.tag_text }}</span>
{% endfor %}
</div>
<hr class="h-px my-4 bg-gray-200 border-0 dark:bg-gray-400" />
<!--Vote View Button-->
<div class="flex items-center text-gray-600">
<button
onclick="window.location.href='{% url 'polls:detail' question.id %}'"
class="mr-2 rounded-md bg-yellow-100 px-2 py-1 text-black font-semibold border-solid border-2 border-yellow-500 hover:bg-yellow-500 transform translate-y-0 hover:translate-y-1 transition-transform">
@ -136,6 +142,7 @@
<h2 class="mb-2 text-xl font-semibold truncate">{{ question.question_text }}</h2>
<hr class="h-px my-2 bg-gray-200 border-0 dark:bg-gray-400" />
<p class="mb-2 text-gray-600">{{ question.short_description }}</p>
<!--Up, Down Vote-->
<div class="mb-2 flex items-center text-gray-600">
<span class="mr-2">👍</span>
<span>{{ question.up_vote_percentage }}% Upvoted</span>
@ -143,12 +150,20 @@
<span class="ml-4 mr-2">👎</span>
<span>{{ question.down_vote_percentage }}% Downvoted</span>
</div>
<!-- Tag / Time -->
<!-- Participant, Time -->
<div class="flex items-center text-gray-600">
<span class="mr-2 rounded-md bg-green-500 px-2 py-1 text-white">🕒 {{ question.time_left }}</span>
<span class="mr-2 rounded-md bg-orange-100 px-2 py-1 text-black">{{ question.participants }} Participants 👤</span>
</div>
<div class="flex items-center text-gray-600 py-4">
<!-- Tags-->
<div class="flex pt-2">
{% for tag in question.tags.all %}
<span class="mr-2 rounded-md bg-blue-100 px-1 py-1 text-blue-400 text-xs text-black font-bold">{{ tag.tag_text }}</span>
{% endfor %}
</div>
<hr class="h-px my-4 bg-gray-200 border-0 dark:bg-gray-400" />
<!--Vote View Button-->
<div class="flex items-center text-gray-600">
<button
onclick="window.location.href='{% url 'polls:detail' question.id %}'"
class="mr-2 rounded-md bg-white px-2 py-1 text-black border-solid border-2 border-black hover:bg-gray-500 transform translate-y-0 hover:translate-y-1 transition-transform">

View File

@ -0,0 +1,110 @@
{% extends "polls/base.html" %}
{% block content %}
<!-- Navbar -->
<nav class="bg-blue-500 p-4 shadow-xl border-b-2 border-solid border-neutral-700">
<div class="container mx-auto flex items-center justify-between">
<div class="text-2xl font-bold text-white">
{% if user.is_authenticated %}
<a href={% url "polls:index" %}>📝BACK TO KU POLL</a> | 👋 Hi! {{ user.username }}
{% else %}
<a href={% url "polls:index" %}>📝BACK TO KU POLL</a>
{% endif %}
</div>
<!-- Button -->
<div>
{% comment %}
<button class="mr-4 rounded-md bg-green-500 px-4 py-2 text-white">New Poll</button>
{% endcomment %}
<div class="flex flex-wrap items-center space-x-2">
<header class="flex items-center justify-center">
<div class="flex flex-wrap items-center space-x-2">
<!--Searchhhh-->
<form action="{% url "polls:search_poll" %}" method="get" class="group relative z-30 flex flex-grow flex-col items-center space-y-2 rounded-lg border-2 border-neutral-700 bg-white p-1 pl-2 text-lg md:flex-row md:space-y-0 md:space-x-1 md:rounded-full">
<div class="flex w-full items-center rounded-lg bg-neutral-200 py-1 px-2 focus-within:outline-none focus-within:ring-2 focus-within:ring-purple-400 md:rounded-full">
<div>🔎</div>
<input type="text" name="q" class="font-semibold w-full border-none bg-transparent focus:outline-none focus:ring focus:ring-transparent text-sm">
</div>
<button type="submit">
<span class="flex items-center whitespace-nowrap rounded-full border border-transparent bg-green-500 px-5 py-2 text-sm font-bold text-white transition duration-150 ease-in-out hover:scale-[101%] hover:bg-green-700 focus:bg-green-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 active:bg-green-900">
Search
</span>
</button>
</form>
<!--End-->
</div>
</header>
{% if user.is_authenticated %}
<a href="{% url "polls:create_poll" %}" class="flex items-center whitespace-nowrap rounded-full border border-transparent bg-green-500 px-5 py-2 text-sm font-bold text-white transition duration-150 ease-in-out hover:scale-[101%] hover:bg-green-700 focus:bg-green-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 active:bg-green-900">
New Poll
</a>
<a href="{% url 'logout' %}"
class="flex items-center whitespace-nowrap rounded-full border border-transparent bg-red-600 px-5 py-2 text-sm font-bold text-white transition duration-150 ease-in-out hover:scale-[101%] hover:bg-red-700 focus:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 active:bg-neutral-900">
<span>Sign out <span class="hidden sm:inline-block">😭</span></span>
</a>
{% else %}
<a href="{% url 'login' %}"
class="flex items-center whitespace-nowrap rounded-full border border-transparent bg-neutral-800 px-5 py-2 text-sm font-bold text-white transition duration-150 ease-in-out hover:scale-[101%] hover:bg-neutral-700 focus:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 active:bg-red-900">
<span>Sign in <span class="hidden sm:inline-block">😎</span></span>
</a>
{% endif %}
</div>
</div>
</div>
</nav>
<div class="bg-white p-4 rounded-lg shadow-md mb-4">
{% if q %}
{% with results.count as total_result %}
<h2 class="mb-4 text-2xl font-bold">Found {{ total_result }} Polls!</h2>
{% endwith %}
{% endif %}
<hr class="h-px my-4 bg-gray-200 border-0 dark:bg-gray-400" />
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3">
{% for question in results %}
<div class="relative">
<!-- INFO -->
<div class="rounded-lg bg-white p-4 shadow-md border-solid border-2 border-neutral-500 relative z-10 transform translate-y-0 hover:translate-y-1 transition-transform">
<h2 class="mb-2 text-xl font-semibold truncate">{{ question.question_text }}</h2>
<hr class="h-px my-2 bg-gray-200 border-0 dark:bg-gray-400" />
<p class="mb-2 text-gray-600">{{ question.short_description }}</p>
<div class="mb-2 flex items-center text-gray-600">
<span class="mr-2">👍</span>
<span>{{ question.up_vote_percentage }}% Upvoted</span>
<span class="ml-4 mr-2">👎</span>
<span>{{ question.down_vote_percentage }}% Downvoted</span>
</div>
<!-- Tag / Time -->
<div class="flex items-center text-gray-600">
<span class="mr-2 rounded-md bg-green-500 px-2 py-1 text-white">🕒 {{ question.time_left }}</span>
<span class="mr-2 rounded-md bg-orange-100 px-2 py-1 text-black">{{ question.participants }} Participants 👤</span>
</div>
<div class="flex items-center text-gray-600 py-4">
<button
onclick="window.location.href='{% url 'polls:detail' question.id %}'"
class="mr-2 rounded-md bg-white px-2 py-1 text-black border-solid border-2 border-black hover:bg-gray-500 transform translate-y-0 hover:translate-y-1 transition-transform">
VOTE
</button>
<button
onclick="window.location.href='{% url 'polls:results' question.id %}'"
class="mr-2 rounded-md bg-white px-2 py-1 text-black border-solid border-2 border-black hover:bg-gray-500 transform translate-y-0 hover:translate-y-1 transition-transform">
VIEW
</button>
</div>
</div>
{% if forloop.counter|divisibleby:2 %}
<div class="absolute inset-0 mt-1 ml-1 h-full w-full rounded-lg border-2 border-neutral-700 bg-gradient-to-r from-green-400 to-blue-500">
</div>
{% else %}
<div class="absolute inset-0 mt-1 ml-1 h-full w-full rounded-lg border-2 border-neutral-700 bg-gradient-to-r from-orange-400 to-red-500">
</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
</section>
</div>
{% endblock content %}

View File

@ -1,7 +1,6 @@
from django.utils import timezone
from django.contrib.auth.models import User
from ..models import Question, Vote, Choice
from ..models import Question
def create_question(question_text, day=0):
@ -11,4 +10,4 @@ def create_question(question_text, day=0):
in the past, positive for questions that have yet to be published).
"""
time = timezone.now() + timezone.timedelta(days=day)
return Question.objects.create(question_text=question_text, pub_date=time)
return Question.objects.create(question_text=question_text, pub_date=time)

View File

@ -1,5 +1,4 @@
from django.test import TestCase, Client
from django.utils import timezone
from django.urls import reverse
from django.contrib.auth.models import User

View File

@ -102,4 +102,4 @@ class QuestionModelTests(TestCase):
pub_date = timezone.now() - datetime.timedelta(hours=1)
end_date = timezone.now() + datetime.timedelta(hours=2)
question = Question(pub_date=pub_date, end_date=end_date)
self.assertIs(question.can_vote(), True)
self.assertIs(question.can_vote(), True)

View File

@ -0,0 +1,23 @@
from django.test import TestCase
from django.urls import reverse
from ..models import Question
class SearchPollTest(TestCase):
"""Test if user search with normal string. It must return same queryset as filter question objects"""
def test_search_normal_poll(self):
data_1 = {'q': 'what'}
data_2 = {'q': 'prefer'}
q_1 = 'what'
q_2 = 'prefer'
response_1 = self.client.get(reverse("polls:search_poll"), data_1)
response_2 = self.client.get(reverse("polls:search_poll"), data_2)
self.assertQuerysetEqual(response_1.context['results'], Question.objects.filter(question_text__icontains=q_1))
self.assertQuerysetEqual(response_2.context['results'], Question.objects.filter(question_text__icontains=q_2))
def test_search_with_empty(self):
"""Test if user search with empty string. It must return all question"""
data = {'q': ''}
response = self.client.get(reverse("polls:search_poll"), data)
self.assertQuerysetEqual(response.context['results'], Question.objects.all())

View File

@ -2,7 +2,7 @@ from django.test import TransactionTestCase, Client
from django.contrib.auth.models import User
from .base import create_question
from ..views import up_down_vote
# ! https://stackoverflow.com/questions/24588520/testing-several-integrityerrors-in-the-same-django-unittest-test-case
# * https://stackoverflow.com/questions/44450533/difference-between-testcase-and-transactiontestcase-classes-in-django-test
@ -32,11 +32,11 @@ class UpDownVoteViewTest(TransactionTestCase):
self.assertEqual(count_up, 0)
self.assertEqual(count_down, 1)
def test_can_change_up_to_down(self):
def test_can_change_down_to_up(self):
self.client.login(username="test_user", password="12345abc")
self.q1.downvote(self.user)
self.q1.upvote(self.user)
count_up = self.q1.sentimentvote_set.filter(vote_types=True).count()
count_down = self.q1.sentimentvote_set.filter(vote_types=False).count()
self.assertEqual(count_up, 1)
self.assertEqual(count_down, 0)
self.assertEqual(count_down, 0)

View File

@ -42,4 +42,4 @@ class SignUpTestCase(TestCase):
'password2': 'testpassword123',
}
response = self.client.post(signup_url, data)
self.assertRedirects(response, reverse("polls:index"))
self.assertRedirects(response, reverse("polls:index"))

View File

@ -5,6 +5,7 @@ from django.contrib.auth.models import User
from .base import create_question
from ..models import Vote, Choice
class VoteViewTest(TestCase):
@classmethod
def setUpTestData(cls):
@ -20,9 +21,9 @@ class VoteViewTest(TestCase):
"""
self.client.login(username=self.user.username, password="aaa123321aaa")
response = self.client.post(reverse("polls:vote", args=(self.question.id,)),
{'choice' : self.choice1.id})
response = self.client.post(reverse("polls:vote", args=(self.question.id,)),
{'choice': self.choice1.id})
self.assertRedirects(response, reverse("polls:results", args=(self.question.id,)))
self.assertTrue(Vote.objects.filter(user=self.user, question=self.question).exists())
@ -32,18 +33,18 @@ class VoteViewTest(TestCase):
"""
self.client.login(username=self.user.username, password="aaa123321aaa")
response = self.client.post(reverse("polls:vote", args=(self.question.id,)),
{'choice' : 1000})
response = self.client.post(reverse("polls:vote", args=(self.question.id,)),
{'choice': 1000})
self.assertRedirects(response, reverse('polls:detail', args=(self.question.id,)))
def test_vote_without_login(self):
"""
Test the vote view when the user is not logged in.
"""
response = self.client.post(reverse("polls:vote", args=(self.question.id,)),
{'choice' : self.choice1})
response = self.client.post(reverse("polls:vote", args=(self.question.id,)),
{'choice': self.choice1})
self.assertRedirects(response, "/accounts/login/?next=/polls/1/vote/")
def test_vote_voting_not_allowed(self):
@ -55,9 +56,8 @@ class VoteViewTest(TestCase):
self.question_2 = create_question(question_text="Test not allow", day=10)
self.choice_2 = Choice.objects.create(question=self.question_2, choice_text="Test Choice 2_2")
response = self.client.post(reverse("polls:vote", args=(self.question_2.id,)),
{"choice" : self.choice_2.id})
response = self.client.post(reverse("polls:vote", args=(self.question_2.id,)), {"choice": self.choice_2.id})
self.assertRedirects(response, reverse('polls:index'))
def test_vote_with_no_post_data(self):
@ -81,6 +81,6 @@ class VoteViewTest(TestCase):
response_2 = self.client.post(reverse("polls:vote", args=(self.question.id,)), {"choice": self.choice2.id})
self.assertRedirects(response_2, reverse('polls:results', args=(self.question.id,)))
self.assertFalse(Vote.objects.filter(user=self.user, question=self.question, choice=self.choice1).exists())
self.assertTrue(Vote.objects.filter(user=self.user, question=self.question, choice=self.choice2).exists())
self.assertTrue(Vote.objects.filter(user=self.user, question=self.question, choice=self.choice2).exists())

View File

@ -9,6 +9,8 @@ urlpatterns = [
path("<int:pk>/results/", views.ResultsView.as_view(), name="results"),
path("<int:question_id>/vote/", views.vote, name="vote"),
path("signup/", views.SignUpView.as_view(), name="signup"),
path("upvote/<int:question_id>", views.up_down_vote, {'vote_type' : 'upvote'}, name="upvote"),
path("downvote/<int:question_id>", views.up_down_vote, {'vote_type' : 'downvote'}, name="downvote"),
path("upvote/<int:question_id>", views.up_down_vote, {'vote_type': 'upvote'}, name="upvote"),
path("downvote/<int:question_id>", views.up_down_vote, {'vote_type': 'downvote'}, name="downvote"),
path("search", views.search_poll, name="search_poll"),
path("create", views.create_poll, name="create_poll")
]

View File

@ -2,7 +2,6 @@ import logging
from typing import Any
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.urls import reverse_lazy, reverse
@ -12,7 +11,7 @@ from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import login_required
from django.db.models import Q
from .forms import SignUpForm
from .forms import SignUpForm, PollSearchForm, PollCreateForm
from .models import Choice, Question, Vote
@ -26,21 +25,21 @@ class IndexView(generic.ListView):
context_object_name = "latest_question_list"
def get_queryset(self):
"""
Return the last published questions that is published and haven't ended yet.
"""
now = timezone.now()
all_poll_queryset = Question.objects.filter(
Q(pub_date__lte=now) & ((Q(end_date__gte=now) | Q(end_date=None)))
).order_by("-pub_date")
"""
Return the last published questions that is published and haven't ended yet.
"""
now = timezone.now()
all_poll_queryset = Question.objects.filter(
Q(pub_date__lte=now) & ((Q(end_date__gte=now) | Q(end_date=None)))
).order_by("-pub_date")
trend_poll_queryset = Question.objects.filter(
Q(pub_date__lte=now) & ((Q(end_date__gte=now) | Q(end_date=None))) & Q(trend_score__gte=100)
).order_by("trend_score", "end_date")[:3]
trend_poll_queryset = Question.objects.filter(
Q(pub_date__lte=now) & ((Q(end_date__gte=now) | Q(end_date=None))) & Q(trend_score__gte=100)
).order_by("trend_score")[:3]
queryset = {'all_poll' : all_poll_queryset,
'trend_poll' : trend_poll_queryset,}
return queryset
queryset = {'all_poll': all_poll_queryset,
'trend_poll': trend_poll_queryset, }
return queryset
class DetailView(LoginRequiredMixin, generic.DetailView):
@ -93,12 +92,16 @@ class DetailView(LoginRequiredMixin, generic.DetailView):
class ResultsView(LoginRequiredMixin, generic.DetailView):
"""
Provide a view for Result page, a Result for the poll contain poll participants
number and other statistic such as up, down vote
"""
model = Question
template_name = "polls/results.html"
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
context = super().get_context_data(**kwargs)
user_voted = None
question = self.get_object()
if question.sentimentvote_set.filter(user=self.request.user, question=question, vote_types=True).exists():
@ -109,7 +112,11 @@ class ResultsView(LoginRequiredMixin, generic.DetailView):
context['user_voted'] = user_voted
return context
class SignUpView(generic.CreateView):
"""
View that responsible for Sign Up page.
"""
form_class = SignUpForm
success_url = reverse_lazy('polls:index')
template_name = 'registration/signup.html'
@ -146,7 +153,7 @@ def vote(request, question_id):
vote, created = Vote.objects.update_or_create(
user=request.user,
question=question,
defaults={'choice' : selected_choice}
defaults={'choice': selected_choice}
)
if created:
@ -163,13 +170,15 @@ def vote(request, question_id):
else:
messages.error(request, "Invalid request method.")
return redirect("polls:index")
@login_required
def up_down_vote(request, question_id, vote_type):
ip = get_client_ip(request)
"""
A function that control the upvote and downvote request.
"""
question = get_object_or_404(Question, pk=question_id)
if request.method == "POST":
if vote_type == "upvote":
if question.upvote(request.user):
@ -177,15 +186,76 @@ def up_down_vote(request, question_id, vote_type):
elif vote_type == "downvote":
if question.downvote(request.user):
messages.success(request, "You downvoted this Poll😭")
return redirect(reverse("polls:results", args=(question_id,)))
# https://stackoverflow.com/questions/4581789/how-do-i-get-user-ip-address-in-django
def get_client_ip(request):
"""
Use with logger to get ip of user.
"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
def search_poll(request):
"""
A function that handle the rendering of search result after user search with
search bar.
"""
form = PollSearchForm
results = []
q = ''
if 'q' in request.GET:
form = PollSearchForm(request.GET)
if form.is_valid():
q = form.cleaned_data['q']
# Case insensitive (icontains)
results = Question.objects.filter(question_text__icontains=q)
# * If user search with empty string then show every poll.
if q == '':
results = Question.objects.all()
return render(request, 'polls/search.html', {'form': form, 'results': results, 'q': q})
@login_required
def create_poll(request):
ip = get_client_ip(request)
if request.method == 'POST':
form = PollCreateForm(request.POST)
if form.is_valid():
question_text = form.cleaned_data['question_text']
pub_date = form.cleaned_data['pub_date']
end_date = form.cleaned_data['end_date']
short_description = form.cleaned_data['short_description']
long_description = form.cleaned_data.get('long_description', '')
user_choices = form.cleaned_data['user_choice']
tags = form.cleaned_data['tags']
question = Question.objects.create(
question_text=question_text,
pub_date=pub_date,
end_date=end_date,
short_description=short_description,
long_description=long_description,
)
choices = user_choices.split(',') # Split with comma
for choice_text in choices:
Choice.objects.create(question=question, choice_text=choice_text.strip())
# Add tags to the question
question.tags.set(tags)
logger.info(f"User {request.user.username} ({ip}) create poll : {question_text}")
return redirect('polls:index')
else:
form = PollCreateForm()
return render(request, 'polls/creation.html', {'form': form})

View File

@ -62,4 +62,5 @@ Write-Host $bottomBorder
python manage.py migrate
python manage.py loaddata data/users.json
python manage.py loaddata data/polls.json
python manage.py loaddata data/vote.json
python manage.py runserver --insecure

View File

@ -17,7 +17,7 @@ def check_python_command():
return command
except FileNotFoundError:
continue
return None
def create_virtual_environment(env_name, python_command):
@ -31,7 +31,7 @@ def customize_virtual_environment():
def setup_environment_variables(python_command_in_venv):
print("Setting up Django environment variables:")
# SECRET KEY
generate_secret_key = input("Generate a Django SECRET_KEY? (yes/no): ").strip().lower()
if generate_secret_key == "yes":
@ -39,7 +39,7 @@ def setup_environment_variables(python_command_in_venv):
'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())']).decode().strip()
else:
secret_key = input("Enter Django SECRET_KEY: ").strip()
# DEBUG MODE
while True:
debug_mode = input("Enable DEBUG mode? (True/False): ").strip()
@ -52,7 +52,7 @@ def setup_environment_variables(python_command_in_venv):
allowed_hosts = input("Enter ALLOWED_HOSTS (comma-separated, or press Enter for default): ").strip()
if not allowed_hosts:
allowed_hosts = "*.ku.th,localhost,127.0.0.1,::1"
# TZ
available_time_zones = ["Asia/Bangkok", "Japan", "UCT", "CST6CDT", "Custom"]
@ -73,7 +73,7 @@ def setup_environment_variables(python_command_in_venv):
print("Invalid choice. Please enter a valid number.")
except ValueError:
print("Invalid input. Please enter a valid number.")
email_host_password = input("Enter EMAIL_HOST_PASSWORD: ").strip()
# SET
@ -116,7 +116,7 @@ def main():
elif is_windows:
activate_command = os.path.join(".venv", "Scripts", "activate")
subprocess.run([activate_command], shell=True)
python_command = os.path.join(".venv", "bin", "python") if is_posix else os.path.join(".venv", "Scripts", "python")
else:
print("Not setting up a virtual environment. Using the global Python interpreter.")
@ -136,6 +136,7 @@ def main():
subprocess.run([python_command, "manage.py", "migrate"])
subprocess.run([python_command, "manage.py", "loaddata", "data/users.json"])
subprocess.run([python_command, "manage.py", "loaddata", "data/polls.json"])
subprocess.run([python_command, "manage.py", "loaddata", "data/vote.json"])
start_server = input("Do you want to start the Django server? (yes/no): ").lower()
if start_server == "yes":
@ -154,19 +155,20 @@ def main():
print(f"==========================Install Requirement==========================")
subprocess.run([python_command_in_venv, "-m", "pip", "install", "-r", "requirements.txt"])
setup_environment_variables(python_command_in_venv)
subprocess.run([python_command_in_venv, "-m", "pip", "install", "-r", "requirements.txt"], check=True)
subprocess.run([python_command_in_venv, "manage.py", "migrate"], check=True)
subprocess.run([python_command_in_venv, "manage.py", "loaddata", "data/users.json"], check=True)
subprocess.run([python_command_in_venv, "manage.py", "loaddata", "data/polls.json"], check=True)
subprocess.run([python_command_in_venv, "manage.py", "loaddata", "data/vote.json"], check=True)
start_server = input("Do you want to start the Django server? (yes/no): ").strip().lower()
if start_server == "yes":
print("=================================================")
print("Django run in --insecure mode to load Static File")
print("==================================================")
subprocess.run([python_command_in_venv, "manage.py", "runserver", "--insecure"], check=True)
except subprocess.CalledProcessError as e:
print(f"Error: {e}")
sys.exit(1)
@ -174,5 +176,6 @@ def main():
print("\nSetup process aborted.")
sys.exit(1)
if __name__ == "__main__":
main()