Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
becfbeb070 | ||
|
|
466555ce05 | ||
|
|
33379408ba |
No files matched your search
+13
@@ -0,0 +1,13 @@
|
|||||||
|
SECRET_KEY=django-insecure-n7=j$-o2$-ae1_xw5$ers@t*zvad97mwo$tjf^imow524_8#=o
|
||||||
|
DEBUG=True
|
||||||
|
|
||||||
|
POSTGRES_DB=askme
|
||||||
|
POSTGRES_USER=askme_user
|
||||||
|
POSTGRES_PASSWORD=askme_password
|
||||||
|
|
||||||
|
DB_NAME=askme
|
||||||
|
DB_USER=askme_user
|
||||||
|
DB_PASSWORD=askme_password
|
||||||
|
DB_HOST=db
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_ENGINE="django.db.backends.postgresql"
|
||||||
+8
-1
@@ -1,2 +1,9 @@
|
|||||||
SECRET_KEY=django-insecure-1234
|
SECRET_KEY=django-insecure-1234
|
||||||
DEBUG=True
|
DEBUG=True
|
||||||
|
|
||||||
|
DB_NAME=vkdesk
|
||||||
|
DB_USER=postgres
|
||||||
|
DB_PASSWORD=1234567
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_ENGINE="django.db.backends.postgresql"
|
||||||
+5
-6
@@ -1,15 +1,14 @@
|
|||||||
FROM python:3.10-slim
|
FROM python:3.11
|
||||||
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY ./requirements.txt ./
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
COPY . .
|
RUN pip install --upgrade pip
|
||||||
|
|
||||||
EXPOSE 8000
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
|
COPY . .
|
||||||
@@ -10,31 +10,19 @@
|
|||||||
🔧 Студент VK Education, курса "Web-разработка".
|
🔧 Студент VK Education, курса "Web-разработка".
|
||||||
|
|
||||||
|
|
||||||
## HW-1:
|
|
||||||
- создан репозиторий и подкаталоги внутри
|
|
||||||
- создана базовая структура проекта (/public/*.html, /public/static/css/style.css, /public/static/js/script.js)
|
|
||||||
- подключена библиотека bootstrap локально (/public/static/css/bootstrap.min.css) (пока не использовалась, подключена на будущее)
|
|
||||||
- настроены права (protected main branch + merge только через PR)
|
|
||||||
<img width="1240" height="1086" alt="image" src="https://github.com/user-attachments/assets/e576c646-e5e6-43dc-82a3-b130f8f7583b" />
|
|
||||||
<img width="1213" height="357" alt="image" src="https://github.com/user-attachments/assets/28d203f8-4a45-4a4b-84d2-ea4f9251bc27" />
|
|
||||||
<img width="656" height="225" alt="image" src="https://github.com/user-attachments/assets/3f714157-1b8e-4fd0-aa14-5396735c8d60" />
|
|
||||||
- настроен .gitignore (за основу взят шаблон для python)
|
|
||||||
- сверстаны base.html, index.html, question.html, ask.html, login.html, signup.html, profile.html
|
|
||||||
>>>>>>> main
|
|
||||||
|
|
||||||
## Запуск проекта
|
## Запуск проекта
|
||||||
|
Перед запуском нужно составить корректный .env файл! Можно использовать .env.example!
|
||||||
|
|
||||||
### 1. Установка корректных .env
|
### На текущей машине
|
||||||
|
|
||||||
Для отладки можно переименовать .env.example в .env
|
|
||||||
|
|
||||||
### 2. На текущей машине
|
|
||||||
```shell
|
```shell
|
||||||
pip install --no-cache-dir -r requirements.txt
|
|
||||||
python manage.py runserver 8000
|
python manage.py runserver 8000
|
||||||
|
pip install --no-cache-dir -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Docker
|
### Docker
|
||||||
|
|
||||||
|
ВАЖНО! Использовать .env.docker вместо .env.example в качестве образца!
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker compose up --build
|
docker compose up --build
|
||||||
@@ -42,3 +30,13 @@ docker compose up --build
|
|||||||
|
|
||||||
Запустится именно на 127.0.0.1:8000
|
Запустится именно на 127.0.0.1:8000
|
||||||
|
|
||||||
|
|
||||||
|
## Миграции и заполнение дб
|
||||||
|
|
||||||
|
Заполнить ДБ 10000 рандомных данных
|
||||||
|
|
||||||
|
```
|
||||||
|
python manage.py makemigrations
|
||||||
|
python manage.py migrate
|
||||||
|
python manage.py fill_db 10000
|
||||||
|
```
|
||||||
+12
-3
@@ -38,6 +38,7 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
|
'questions',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@@ -80,8 +81,12 @@ WSGI_APPLICATION = 'application.wsgi.application'
|
|||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': os.getenv('DB_ENGINE'),
|
||||||
'NAME': BASE_DIR / 'db.sqlite3',
|
'NAME': os.getenv('DB_NAME'),
|
||||||
|
'USER': os.getenv('DB_USER'),
|
||||||
|
'PASSWORD': os.getenv('DB_PASSWORD'),
|
||||||
|
'HOST': os.getenv('DB_HOST'),
|
||||||
|
'PORT': os.getenv('DB_PORT'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +113,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||||
|
|
||||||
LANGUAGE_CODE = 'en-us'
|
LANGUAGE_CODE = 'ru-RU'
|
||||||
|
|
||||||
TIME_ZONE = 'UTC'
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
@@ -128,3 +133,7 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
|||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
MEDIA_ROOT = BASE_DIR / 'media'
|
||||||
+60
-1
@@ -1,11 +1,70 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
|
db:
|
||||||
|
|
||||||
|
image: postgres:16
|
||||||
|
|
||||||
|
container_name: postgres-container
|
||||||
|
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- django-network
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
python-app:
|
python-app:
|
||||||
|
|
||||||
build: .
|
build: .
|
||||||
|
|
||||||
container_name: django-app-container
|
container_name: django-app-container
|
||||||
|
|
||||||
|
restart: always
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
|
|
||||||
|
working_dir: /app
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
working_dir: /app
|
- media_data:/app/media
|
||||||
|
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
|
command: >
|
||||||
|
sh -c "
|
||||||
|
python manage.py migrate &&
|
||||||
|
python manage.py runserver 0.0.0.0:8000
|
||||||
|
"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- django-network
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
media_data:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
networks:
|
||||||
|
django-network:
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.contrib.auth import authenticate
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.auth.password_validation import validate_password
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from .models import Question, Tag
|
||||||
|
from .models import Profile
|
||||||
|
from .models import Answer
|
||||||
|
|
||||||
|
def validate_avatar(image):
|
||||||
|
max_size = 2 * 1024 * 1024
|
||||||
|
|
||||||
|
if image.size > max_size:
|
||||||
|
raise ValidationError("Image too large (max 2MB)")
|
||||||
|
|
||||||
|
ext = os.path.splitext(image.name)[1].lower()
|
||||||
|
|
||||||
|
allowed_extensions = ['.jpg', '.jpeg', '.png', '.webp', '.ico']
|
||||||
|
|
||||||
|
if ext not in allowed_extensions:
|
||||||
|
raise ValidationError("Unsupported file extension")
|
||||||
|
|
||||||
|
class LoginForm(forms.Form):
|
||||||
|
username = forms.CharField(max_length=150)
|
||||||
|
password = forms.CharField(widget=forms.PasswordInput)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
|
||||||
|
username = cleaned_data.get('username')
|
||||||
|
password = cleaned_data.get('password')
|
||||||
|
|
||||||
|
user = authenticate(username=username, password=password)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
raise ValidationError("Wrong login or password")
|
||||||
|
|
||||||
|
cleaned_data['user'] = user
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
class SignupForm(forms.Form):
|
||||||
|
username = forms.CharField(max_length=150)
|
||||||
|
email = forms.EmailField()
|
||||||
|
nickname = forms.CharField(max_length=100)
|
||||||
|
|
||||||
|
password = forms.CharField(widget=forms.PasswordInput)
|
||||||
|
password_repeat = forms.CharField(widget=forms.PasswordInput)
|
||||||
|
|
||||||
|
avatar = forms.ImageField(
|
||||||
|
required=False,
|
||||||
|
validators=[validate_avatar]
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean_username(self):
|
||||||
|
username = self.cleaned_data['username']
|
||||||
|
if User.objects.filter(username=username).exists():
|
||||||
|
raise ValidationError("Username already exists")
|
||||||
|
return username
|
||||||
|
|
||||||
|
def clean_email(self):
|
||||||
|
email = self.cleaned_data['email']
|
||||||
|
if Profile.objects.filter(email=email).exists():
|
||||||
|
raise ValidationError("Email already registered")
|
||||||
|
return email
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
|
||||||
|
p1 = cleaned_data.get('password')
|
||||||
|
p2 = cleaned_data.get('password_repeat')
|
||||||
|
|
||||||
|
if p1 != p2:
|
||||||
|
raise ValidationError("Passwords do not match")
|
||||||
|
|
||||||
|
validate_password(p1)
|
||||||
|
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
user = User.objects.create_user(
|
||||||
|
username=self.cleaned_data['username'],
|
||||||
|
password=self.cleaned_data['password']
|
||||||
|
)
|
||||||
|
|
||||||
|
Profile.objects.create(
|
||||||
|
user=user,
|
||||||
|
email=self.cleaned_data['email'],
|
||||||
|
nickname=self.cleaned_data['nickname'],
|
||||||
|
avatar=self.cleaned_data.get('avatar')
|
||||||
|
)
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileForm(forms.Form):
|
||||||
|
email = forms.EmailField()
|
||||||
|
nickname = forms.CharField(max_length=100)
|
||||||
|
avatar = forms.ImageField(
|
||||||
|
required=False,
|
||||||
|
validators=[validate_avatar]
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, user=None, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.user = user
|
||||||
|
|
||||||
|
if user:
|
||||||
|
self.fields['email'].initial = user.profile.email
|
||||||
|
self.fields['nickname'].initial = user.profile.nickname
|
||||||
|
|
||||||
|
def clean_email(self):
|
||||||
|
email = self.cleaned_data['email']
|
||||||
|
|
||||||
|
if Profile.objects.filter(email=email).exclude(user=self.user).exists():
|
||||||
|
raise ValidationError("Email already used")
|
||||||
|
|
||||||
|
return email
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
profile = self.user.profile
|
||||||
|
|
||||||
|
profile.email = self.cleaned_data['email']
|
||||||
|
profile.nickname = self.cleaned_data['nickname']
|
||||||
|
|
||||||
|
if self.cleaned_data.get('avatar'):
|
||||||
|
profile.avatar = self.cleaned_data['avatar']
|
||||||
|
|
||||||
|
profile.save()
|
||||||
|
|
||||||
|
class AskForm(forms.ModelForm):
|
||||||
|
tags = forms.CharField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Question
|
||||||
|
fields = ['title', 'body']
|
||||||
|
|
||||||
|
def clean_tags(self):
|
||||||
|
tags = self.cleaned_data['tags'].split(',')
|
||||||
|
tags = [t.strip() for t in tags if t.strip()]
|
||||||
|
|
||||||
|
if not tags:
|
||||||
|
raise ValidationError("Add at least one tag")
|
||||||
|
|
||||||
|
return tags
|
||||||
|
|
||||||
|
def save(self, user):
|
||||||
|
question = super().save(commit=False)
|
||||||
|
question.user = user
|
||||||
|
question.save()
|
||||||
|
|
||||||
|
tags = self.cleaned_data['tags']
|
||||||
|
|
||||||
|
for tag_name in tags:
|
||||||
|
tag, _ = Tag.objects.get_or_create(name=tag_name)
|
||||||
|
question.tags.add(tag)
|
||||||
|
|
||||||
|
return question
|
||||||
|
|
||||||
|
class AnswerForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Answer
|
||||||
|
fields = ['body']
|
||||||
|
|
||||||
|
def save(self, user, question):
|
||||||
|
answer = super().save(commit=False)
|
||||||
|
answer.user = user
|
||||||
|
answer.question = question
|
||||||
|
answer.save()
|
||||||
|
return answer
|
||||||
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,189 @@
|
|||||||
|
import random
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from faker import Faker
|
||||||
|
|
||||||
|
from questions.models import (
|
||||||
|
Question,
|
||||||
|
Answer,
|
||||||
|
Tag,
|
||||||
|
Profile,
|
||||||
|
QuestionReaction,
|
||||||
|
)
|
||||||
|
|
||||||
|
fake = Faker()
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Fill database with test data'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('ratio', type=int)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
ratio = options['ratio']
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(f'Start filling DB with ratio={ratio}')
|
||||||
|
)
|
||||||
|
|
||||||
|
self.clear_db()
|
||||||
|
|
||||||
|
users = self.create_users(ratio)
|
||||||
|
tags = self.create_tags(ratio)
|
||||||
|
questions = self.create_questions(ratio, users, tags)
|
||||||
|
|
||||||
|
self.create_answers(ratio, users, questions)
|
||||||
|
self.create_reactions(ratio, users, questions)
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS('DONE'))
|
||||||
|
|
||||||
|
def clear_db(self):
|
||||||
|
QuestionReaction.objects.all().delete()
|
||||||
|
Answer.objects.all().delete()
|
||||||
|
Question.objects.all().delete()
|
||||||
|
Profile.objects.all().delete()
|
||||||
|
Tag.objects.all().delete()
|
||||||
|
|
||||||
|
# если нужен superuser — можно удалить эту строку
|
||||||
|
User.objects.filter(is_superuser=False).delete()
|
||||||
|
|
||||||
|
def create_users(self, ratio):
|
||||||
|
fake.unique.clear()
|
||||||
|
|
||||||
|
users = []
|
||||||
|
|
||||||
|
for i in range(ratio):
|
||||||
|
users.append(
|
||||||
|
User(
|
||||||
|
username=f'user_{i}_{fake.user_name()}',
|
||||||
|
email=fake.unique.email(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
User.objects.bulk_create(users, batch_size=1000)
|
||||||
|
|
||||||
|
users = list(User.objects.all())
|
||||||
|
|
||||||
|
profiles = [
|
||||||
|
Profile(
|
||||||
|
user=user,
|
||||||
|
email=user.email,
|
||||||
|
nickname=fake.user_name(),
|
||||||
|
)
|
||||||
|
for user in users
|
||||||
|
]
|
||||||
|
|
||||||
|
Profile.objects.bulk_create(profiles, batch_size=1000)
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS('Users created'))
|
||||||
|
|
||||||
|
return users
|
||||||
|
|
||||||
|
def create_tags(self, ratio):
|
||||||
|
fake.unique.clear()
|
||||||
|
|
||||||
|
tag_count = max(20, ratio // 100)
|
||||||
|
|
||||||
|
tags = [
|
||||||
|
Tag(name=fake.unique.word())
|
||||||
|
for _ in range(tag_count)
|
||||||
|
]
|
||||||
|
|
||||||
|
Tag.objects.bulk_create(tags, batch_size=1000)
|
||||||
|
|
||||||
|
tags = list(Tag.objects.all())
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS('Tags created'))
|
||||||
|
|
||||||
|
return tags
|
||||||
|
|
||||||
|
def create_questions(self, ratio, users, tags):
|
||||||
|
questions = [
|
||||||
|
Question(
|
||||||
|
title=fake.sentence()[:50],
|
||||||
|
body=fake.text(max_nb_chars=500),
|
||||||
|
user=random.choice(users),
|
||||||
|
rating=random.randint(0, 100),
|
||||||
|
)
|
||||||
|
for _ in range(ratio)
|
||||||
|
]
|
||||||
|
|
||||||
|
Question.objects.bulk_create(questions, batch_size=1000)
|
||||||
|
|
||||||
|
questions = list(Question.objects.all())
|
||||||
|
|
||||||
|
# BULK CREATE ДЛЯ MANY-TO-MANY
|
||||||
|
through_model = Question.tags.through
|
||||||
|
relations = []
|
||||||
|
|
||||||
|
for question in questions:
|
||||||
|
selected_tags = random.sample(
|
||||||
|
tags,
|
||||||
|
k=min(3, len(tags))
|
||||||
|
)
|
||||||
|
|
||||||
|
for tag in selected_tags:
|
||||||
|
relations.append(
|
||||||
|
through_model(
|
||||||
|
question_id=question.id,
|
||||||
|
tag_id=tag.id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
through_model.objects.bulk_create(
|
||||||
|
relations,
|
||||||
|
batch_size=1000
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS('Questions created'))
|
||||||
|
|
||||||
|
return questions
|
||||||
|
|
||||||
|
def create_answers(self, ratio, users, questions):
|
||||||
|
answers = [
|
||||||
|
Answer(
|
||||||
|
question=random.choice(questions),
|
||||||
|
user=random.choice(users),
|
||||||
|
body=fake.text(max_nb_chars=300),
|
||||||
|
rating=random.randint(0, 50),
|
||||||
|
)
|
||||||
|
for _ in range(ratio)
|
||||||
|
]
|
||||||
|
|
||||||
|
Answer.objects.bulk_create(answers, batch_size=1000)
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS('Answers created'))
|
||||||
|
|
||||||
|
def create_reactions(self, ratio, users, questions):
|
||||||
|
reactions = []
|
||||||
|
used_pairs = set()
|
||||||
|
|
||||||
|
target = ratio
|
||||||
|
|
||||||
|
while len(reactions) < target:
|
||||||
|
user = random.choice(users)
|
||||||
|
question = random.choice(questions)
|
||||||
|
|
||||||
|
pair = (user.id, question.id)
|
||||||
|
|
||||||
|
if pair in used_pairs:
|
||||||
|
continue
|
||||||
|
|
||||||
|
used_pairs.add(pair)
|
||||||
|
|
||||||
|
reactions.append(
|
||||||
|
QuestionReaction(
|
||||||
|
question=question,
|
||||||
|
user=user,
|
||||||
|
positive=random.choice([True, False]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
QuestionReaction.objects.bulk_create(
|
||||||
|
reactions,
|
||||||
|
batch_size=1000
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS('Reactions created'))
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Generated by Django 5.2.12 on 2026-04-27 17:22
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Tag',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=50, unique=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Profile',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('avatar', models.ImageField(blank=True, null=True, upload_to='avatars/')),
|
||||||
|
('email', models.EmailField(max_length=254, unique=True)),
|
||||||
|
('nickname', models.CharField(max_length=100)),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Question',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=50)),
|
||||||
|
('body', models.TextField()),
|
||||||
|
('rating', models.IntegerField(default=0)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
('tags', models.ManyToManyField(to='questions.tag')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Answer',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('body', models.TextField()),
|
||||||
|
('rating', models.IntegerField(default=0)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='questions.question')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='QuestionReaction',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('positive', models.BooleanField(default=True)),
|
||||||
|
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reactions', to='questions.question')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('question', 'user')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Generated by Django 5.2.12 on 2026-05-08 21:19
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import questions.models
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('questions', '0001_initial'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='profile',
|
||||||
|
name='avatar',
|
||||||
|
field=models.ImageField(blank=True, null=True, upload_to=questions.models.avatar_upload_path),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AnswerReaction',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('positive', models.BooleanField(default=True)),
|
||||||
|
('answer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reactions', to='questions.answer')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('answer', 'user')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.2.12 on 2026-05-10 13:57
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('questions', '0002_alter_profile_avatar_answerreaction'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='question',
|
||||||
|
name='correct_answer',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='correct_for', to='questions.answer'),
|
||||||
|
),
|
||||||
|
]
|
||||||
+158
-1
@@ -1,3 +1,160 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
# Create your models here.
|
import uuid
|
||||||
|
import os
|
||||||
|
|
||||||
|
from .utils import crop_square
|
||||||
|
|
||||||
|
|
||||||
|
def avatar_upload_path(instance, filename):
|
||||||
|
|
||||||
|
ext = filename.split('.')[-1]
|
||||||
|
|
||||||
|
filename = f"{uuid.uuid4()}.{ext}"
|
||||||
|
|
||||||
|
return os.path.join('avatars', filename)
|
||||||
|
|
||||||
|
|
||||||
|
class Profile(models.Model):
|
||||||
|
|
||||||
|
user = models.OneToOneField(
|
||||||
|
User,
|
||||||
|
on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
|
avatar = models.ImageField(
|
||||||
|
upload_to=avatar_upload_path,
|
||||||
|
null=True,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
|
||||||
|
email = models.EmailField(unique=True)
|
||||||
|
|
||||||
|
nickname = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.user.username
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
if self.avatar:
|
||||||
|
crop_square(self.avatar.path)
|
||||||
|
|
||||||
|
|
||||||
|
class Tag(models.Model):
|
||||||
|
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Question(models.Model):
|
||||||
|
|
||||||
|
title = models.CharField(max_length=50)
|
||||||
|
|
||||||
|
user = models.ForeignKey(
|
||||||
|
User,
|
||||||
|
on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
|
body = models.TextField()
|
||||||
|
|
||||||
|
rating = models.IntegerField(default=0)
|
||||||
|
|
||||||
|
tags = models.ManyToManyField(Tag)
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(
|
||||||
|
auto_now_add=True
|
||||||
|
)
|
||||||
|
|
||||||
|
correct_answer = models.ForeignKey(
|
||||||
|
'Answer',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name='correct_for'
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionReaction(models.Model):
|
||||||
|
|
||||||
|
question = models.ForeignKey(
|
||||||
|
Question,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='reactions'
|
||||||
|
)
|
||||||
|
|
||||||
|
user = models.ForeignKey(
|
||||||
|
User,
|
||||||
|
on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
|
positive = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('question', 'user')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
|
||||||
|
return (
|
||||||
|
"Reaction for "
|
||||||
|
+ self.question.title
|
||||||
|
+ " by "
|
||||||
|
+ self.user.username
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Answer(models.Model):
|
||||||
|
|
||||||
|
question = models.ForeignKey(
|
||||||
|
Question,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='answers'
|
||||||
|
)
|
||||||
|
|
||||||
|
user = models.ForeignKey(
|
||||||
|
User,
|
||||||
|
on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
|
body = models.TextField()
|
||||||
|
|
||||||
|
rating = models.IntegerField(default=0)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
|
||||||
|
return (
|
||||||
|
"Answer for "
|
||||||
|
+ self.question.title
|
||||||
|
+ " by "
|
||||||
|
+ self.user.username
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AnswerReaction(models.Model):
|
||||||
|
|
||||||
|
answer = models.ForeignKey(
|
||||||
|
Answer,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='reactions'
|
||||||
|
)
|
||||||
|
|
||||||
|
user = models.ForeignKey(
|
||||||
|
User,
|
||||||
|
on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
|
positive = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('answer', 'user')
|
||||||
@@ -204,6 +204,10 @@ a:hover {
|
|||||||
box-shadow: 0.25rem 0.25rem 0 rgba(31, 45, 58, 0.08);
|
box-shadow: 0.25rem 0.25rem 0 rgba(31, 45, 58, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.not-underlined {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar h2 {
|
.sidebar h2 {
|
||||||
margin: 0 0 0.5rem;
|
margin: 0 0 0.5rem;
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
@@ -307,6 +311,21 @@ a:hover {
|
|||||||
margin-top: 0.4rem;
|
margin-top: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.current-tag {
|
||||||
|
display: inline-block;
|
||||||
|
border: 0.125rem solid #1f2d3a;
|
||||||
|
background: #f2f7f7;
|
||||||
|
padding: 0.25rem 0.6rem;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
box-shadow: 0.2rem 0.2rem 0 rgba(31, 45, 58, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-tag span {
|
||||||
|
color: #1d57d4;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
border-top: 0.125rem solid #1f2d3a;
|
border-top: 0.125rem solid #1f2d3a;
|
||||||
padding: 0.6rem 1.1rem;
|
padding: 0.6rem 1.1rem;
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
function getCookie(name) {
|
||||||
|
let cookieValue = null;
|
||||||
|
if (document.cookie && document.cookie !== '') {
|
||||||
|
const cookies = document.cookie.split(';');
|
||||||
|
for (let cookie of cookies) {
|
||||||
|
cookie = cookie.trim();
|
||||||
|
if (cookie.startsWith(name + '=')) {
|
||||||
|
cookieValue = decodeURIComponent(
|
||||||
|
cookie.substring(name.length + 1)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookieValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const csrftoken = getCookie('csrftoken');
|
||||||
|
|
||||||
|
$(document).on('click', '.question-like', function () {
|
||||||
|
const btn = $(this);
|
||||||
|
const questionId = btn.data('id');
|
||||||
|
$.ajax({
|
||||||
|
url: '/ajax/question/react/',
|
||||||
|
type: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': csrftoken
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
question_id: questionId,
|
||||||
|
type: 'like'
|
||||||
|
},
|
||||||
|
success: function (response) {
|
||||||
|
$('#question-rating-' + questionId)
|
||||||
|
.text(response.rating);
|
||||||
|
btn.remove();
|
||||||
|
$('.question-dislike[data-id="' + questionId + '"]')
|
||||||
|
.remove();
|
||||||
|
$('#question-rating-' + questionId)
|
||||||
|
.after('<span> voted </span>');
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
if (xhr.responseJSON) {
|
||||||
|
alert(xhr.responseJSON.error);
|
||||||
|
} else {
|
||||||
|
alert('Server error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.question-dislike', function () {
|
||||||
|
const btn = $(this);
|
||||||
|
const questionId = btn.data('id');
|
||||||
|
$.ajax({
|
||||||
|
url: '/ajax/question/react/',
|
||||||
|
type: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': csrftoken
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
question_id: questionId,
|
||||||
|
type: 'dislike'
|
||||||
|
},
|
||||||
|
success: function (response) {
|
||||||
|
$('#question-rating-' + questionId)
|
||||||
|
.text(response.rating);
|
||||||
|
btn.remove();
|
||||||
|
$('.question-like[data-id="' + questionId + '"]')
|
||||||
|
.remove();
|
||||||
|
$('#question-rating-' + questionId)
|
||||||
|
.after('<span> voted </span>');
|
||||||
|
}
|
||||||
|
error: function (xhr) {
|
||||||
|
if (xhr.responseJSON) {
|
||||||
|
alert(xhr.responseJSON.error);
|
||||||
|
} else {
|
||||||
|
alert('Server error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.answer-like', function () {
|
||||||
|
const btn = $(this);
|
||||||
|
const answerId = btn.data('id');
|
||||||
|
$.ajax({
|
||||||
|
url: '/ajax/answer/react/',
|
||||||
|
type: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': csrftoken
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
answer_id: answerId,
|
||||||
|
type: 'like'
|
||||||
|
}
|
||||||
|
success: function (response) {
|
||||||
|
$('#answer-rating-' + answerId)
|
||||||
|
.text(response.rating);
|
||||||
|
btn.remove();
|
||||||
|
$('.answer-dislike[data-id="' + answerId + '"]')
|
||||||
|
.remove();
|
||||||
|
$('#answer-rating-' + answerId)
|
||||||
|
.after('<span> voted </span>');
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
if (xhr.responseJSON) {
|
||||||
|
alert(xhr.responseJSON.error);
|
||||||
|
} else {
|
||||||
|
alert('Server error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.answer-dislike', function () {
|
||||||
|
const btn = $(this);
|
||||||
|
const answerId = btn.data('id');
|
||||||
|
$.ajax({
|
||||||
|
url: '/ajax/answer/react/',
|
||||||
|
type: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': csrftoken
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
answer_id: answerId,
|
||||||
|
type: 'dislike'
|
||||||
|
},
|
||||||
|
success: function (response) {
|
||||||
|
$('#answer-rating-' + answerId)
|
||||||
|
.text(response.rating);
|
||||||
|
btn.remove();
|
||||||
|
$('.answer-like[data-id="' + answerId + '"]')
|
||||||
|
.remove();
|
||||||
|
|
||||||
|
$('#answer-rating-' + answerId)
|
||||||
|
.after('<span> voted </span>');
|
||||||
|
},
|
||||||
|
|
||||||
|
error: function (xhr) {
|
||||||
|
|
||||||
|
if (xhr.responseJSON) {
|
||||||
|
alert(xhr.responseJSON.error);
|
||||||
|
} else {
|
||||||
|
alert('Server error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$(document).on('click', '.correct-answer-btn', function () {
|
||||||
|
const btn = $(this);
|
||||||
|
$.ajax({
|
||||||
|
url: '/ajax/answer/correct/',
|
||||||
|
type: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': csrftoken
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
question_id: btn.data('question'),
|
||||||
|
answer_id: btn.data('answer')
|
||||||
|
},
|
||||||
|
success: function (response) {
|
||||||
|
$('.correct-label').remove();
|
||||||
|
$('#answer-' + response.correct_answer_id)
|
||||||
|
.find('.answer-controls')
|
||||||
|
.html(`
|
||||||
|
<span class="correct-label">
|
||||||
|
✔ Correct
|
||||||
|
</span>
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
if (xhr.responseJSON) {
|
||||||
|
alert(xhr.responseJSON.error);
|
||||||
|
} else {
|
||||||
|
alert('Server error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,22 +6,29 @@
|
|||||||
<div class="main-title">
|
<div class="main-title">
|
||||||
<h1>Ask a Question</h1>
|
<h1>Ask a Question</h1>
|
||||||
</div>
|
</div>
|
||||||
|
<form class="form" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
|
||||||
<form class="form">
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="title">Title</label>
|
<label>Title</label>
|
||||||
<input id="title" type="text" value="How to build a moon park ?">
|
{{ form.title }}
|
||||||
|
{{ form.title.errors }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="text">Text</label>
|
<label>Text</label>
|
||||||
<textarea id="text">Really, how ? Have no idea about it</textarea>
|
{{ form.body }}
|
||||||
|
{{ form.body.errors }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="tags">Tags</label>
|
<label>Tags</label>
|
||||||
<input id="tags" type="text" value="moon, park, puzzle">
|
{{ form.tags }}
|
||||||
</div>
|
{{ form.tags.errors }}
|
||||||
<div>
|
|
||||||
<button class="button" type="button">ASK IT</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button class="button" type="submit">ASK IT</button>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>VkDesk | Fresh Questions</title>
|
|
||||||
<meta name="description" content="VkDesk Q&A">
|
|
||||||
<link rel="stylesheet" href="../../static/core/css/style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header class="site-header">
|
|
||||||
<div class="logo">VkDesk</div>
|
|
||||||
<form class="search">
|
|
||||||
<input type="text" placeholder="Search">
|
|
||||||
<a class="button button--ask" href="index">ASK IT</a>
|
|
||||||
</form>
|
|
||||||
<div class="user-block">
|
|
||||||
<div class="avatar-box small">avatar</div>
|
|
||||||
<div class="user-links">
|
|
||||||
<div class="user-name">Dr. Pepper</div>
|
|
||||||
<div>
|
|
||||||
<a href="profile">settings</a>
|
|
||||||
<a href="login">log out</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
<main>
|
|
||||||
<div class="main-title">
|
|
||||||
<h1>Fresh Questions</h1>
|
|
||||||
<a href="#">Top Today</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<article class="card">
|
|
||||||
<div>
|
|
||||||
<div class="avatar-box">avatar</div>
|
|
||||||
<div class="vote-box">
|
|
||||||
<span>5</span>
|
|
||||||
<span class="vote-arrows">^<span>'</span></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a class="question-title" href="question.html">How to build a moon park ?</a>
|
|
||||||
<p>Guys, i have trouble with a moon park. Cant find the black-jack...</p>
|
|
||||||
<div class="meta">
|
|
||||||
<a href="question.html">answer (3)</a>
|
|
||||||
<span>Tags:</span>
|
|
||||||
<a class="tag" href="#">black-jack</a>
|
|
||||||
<a class="tag" href="#">bender</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="card">
|
|
||||||
<div>
|
|
||||||
<div class="avatar-box">avatar</div>
|
|
||||||
<div class="vote-box">
|
|
||||||
<span>5</span>
|
|
||||||
<span class="vote-arrows">^<span>'</span></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a class="question-title" href="question">How to build a moon park ?</a>
|
|
||||||
<p>Guys, i have trouble with a moon park. Can't find the black-jack...</p>
|
|
||||||
<div class="meta">
|
|
||||||
<a href="question">answer (3)</a>
|
|
||||||
<span>Tags:</span>
|
|
||||||
<a class="tag" href="#">black-jack</a>
|
|
||||||
<a class="tag" href="#">bender</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="card">
|
|
||||||
<div>
|
|
||||||
<div class="avatar-box">avatar</div>
|
|
||||||
<div class="vote-box">
|
|
||||||
<span>5</span>
|
|
||||||
<span class="vote-arrows">^<span>'</span></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a class="question-title" href="question.html">How to build a moon park ?</a>
|
|
||||||
<p>Guys, i have trouble with a moon park. Can't find the black-jack...</p>
|
|
||||||
<div class="meta">
|
|
||||||
<a href="question.html">answer (3)</a>
|
|
||||||
<span>Tags:</span>
|
|
||||||
<a class="tag" href="#">black-jack</a>
|
|
||||||
<a class="tag" href="#">bender</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<nav class="pagination">
|
|
||||||
<a class="page-link" href="#">1</a>
|
|
||||||
<a class="page-link" href="#">2</a>
|
|
||||||
<a class="page-link" href="#">3</a>
|
|
||||||
<span class="page-link">...</span>
|
|
||||||
<a class="page-link" href="#">53</a>
|
|
||||||
</nav>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<aside class="sidebar">
|
|
||||||
<div class="sidebar-section">
|
|
||||||
<h2>Trending Tags</h2>
|
|
||||||
<div class="tag-cloud">
|
|
||||||
<span>perl</span>
|
|
||||||
<span class="tag-red">c++</span>
|
|
||||||
<span>TechnoPark</span>
|
|
||||||
<span class="tag-red">RUST</span>
|
|
||||||
<span class="tag-green">django</span>
|
|
||||||
<span>Mail.Ru</span>
|
|
||||||
<span>Java</span>
|
|
||||||
<span class="tag-orange">TS</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sidebar-section">
|
|
||||||
<h2>Top Members</h2>
|
|
||||||
<div class="member-list">
|
|
||||||
<a href="#">Mr. Freeman</a>
|
|
||||||
<a href="#">Dr. House</a>
|
|
||||||
<a href="#">Bender</a>
|
|
||||||
<a href="#">Queen Victoria</a>
|
|
||||||
<a href="#">V. Pupkin</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="footer">2026</footer>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{% extends 'core/layout/base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="main-title">
|
||||||
|
<h1>Hot Questions</h1>
|
||||||
|
<a href="/">All questions</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for question in hot_questions %}
|
||||||
|
{% include 'core/partials/question.html' %}
|
||||||
|
{% endfor %}
|
||||||
|
{% include 'core/partials/pagination.html' %}
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -3,10 +3,15 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="main-title">
|
<div class="main-title">
|
||||||
<h1>Fresh Questions</h1>
|
<h1>All Questions</h1>
|
||||||
<a href="#">Top Today</a>
|
<a href="/hot">Hot questions</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if tag %}
|
||||||
|
<div class="current-tag">
|
||||||
|
Текущий тег: <span>#{{ tag }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% for question in questions %}
|
{% for question in questions %}
|
||||||
{% include 'core/partials/question.html' %}
|
{% include 'core/partials/question.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -6,22 +6,37 @@
|
|||||||
<title>VkDesk | Fresh Questions</title>
|
<title>VkDesk | Fresh Questions</title>
|
||||||
<meta name="description" content="VkDesk Q&A">
|
<meta name="description" content="VkDesk Q&A">
|
||||||
<link rel="stylesheet" href="{% static 'core/css/style.css' %}">
|
<link rel="stylesheet" href="{% static 'core/css/style.css' %}">
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<div class="logo">VkDesk</div>
|
<a href="/" class="logo not-underlined">VkDesk</a>
|
||||||
<form class="search">
|
<form class="search">
|
||||||
<input type="text" placeholder="Search">
|
<input type="text" placeholder="Search">
|
||||||
<a class="button button--ask" href="ask">ASK IT</a>
|
<a class="button button--ask" href="ask">ASK IT</a>
|
||||||
</form>
|
</form>
|
||||||
<div class="user-block">
|
<div class="user-block">
|
||||||
<div class="avatar-box small">avatar</div>
|
<div class="avatar-box small">
|
||||||
|
{% if request.user.profile.avatar %}
|
||||||
|
<img src="{{ request.user.profile.avatar.url }}" width="40">
|
||||||
|
{% else %}
|
||||||
|
<img src="{% static 'core/img/profile.webp' %}" width="40">
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<div class="user-links">
|
<div class="user-links">
|
||||||
<a href="profile" class="user-name">Dr. Pepper</a>
|
{% if request.user.is_authenticated %}
|
||||||
|
<a href="{% url 'profile' %}" class="user-name">
|
||||||
|
{{ request.user.profile.nickname }}
|
||||||
|
</a>
|
||||||
<div>
|
<div>
|
||||||
<a href="profile">settings</a>
|
<a href="{% url 'logout' %}">log out</a>
|
||||||
<a href="login">log out</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div>
|
||||||
|
<a href="{% url 'login' %}">log in</a>
|
||||||
|
<a href="{% url 'signup' %}">sign up</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -34,31 +49,16 @@
|
|||||||
<div class="sidebar-section">
|
<div class="sidebar-section">
|
||||||
<h2>Trending Tags</h2>
|
<h2>Trending Tags</h2>
|
||||||
<div class="tag-cloud">
|
<div class="tag-cloud">
|
||||||
<span>perl</span>
|
{% for tag in tags %}
|
||||||
<span class="tag-red">python</span>
|
<a href="/?tag={{ tag }}" class="not-underlined">{{tag}}</a>
|
||||||
<span>TechnoPark</span>
|
{% endfor %}
|
||||||
<span class="tag-red">MySQL</span>
|
|
||||||
<span class="tag-green">django</span>
|
|
||||||
<span>Mail.Ru</span>
|
|
||||||
<span>Voloshin</span>
|
|
||||||
<span class="tag-orange">Firefox</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sidebar-section">
|
|
||||||
<h2>Top Members</h2>
|
|
||||||
<div class="member-list">
|
|
||||||
<a href="#">Mr. Freeman</a>
|
|
||||||
<a href="#">Dr. House</a>
|
|
||||||
<a href="#">Bender</a>
|
|
||||||
<a href="#">Queen Victoria</a>
|
|
||||||
<a href="#">V. Pupkin</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
<footer class="footer">2026</footer>
|
<footer class="footer">2026</footer>
|
||||||
</body>
|
</body>
|
||||||
|
<script src="{% static 'core/js/script.js' %}"></script>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,21 +8,31 @@
|
|||||||
<h1>Sign In</h1>
|
<h1>Sign In</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="error">Sorry, wrong password!</p>
|
|
||||||
|
|
||||||
<form class="form">
|
<form class="form" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="login">Login</label>
|
<label>Login</label>
|
||||||
<input id="login" type="text" placeholder="Enter your login here">
|
{{ form.username }}
|
||||||
|
{{ form.username.errors }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="password">Password</label>
|
<label>Password</label>
|
||||||
<input id="password" type="password" value="********">
|
{{ form.password }}
|
||||||
|
{{ form.password.errors }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button class="button" type="button">Sign In!</button>
|
<button class="button" type="submit">Sign In!</button>
|
||||||
</div>
|
</div>
|
||||||
<a href="signup">create new account</a>
|
|
||||||
|
<a href="{% url 'signup' %}">create new account</a>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
<nav class="pagination">
|
<nav class="pagination">
|
||||||
|
{% if page_obj.paginator.num_pages > 1 %}
|
||||||
{% if page_obj.has_previous %}
|
{% if page_obj.has_previous %}
|
||||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">Previous</a>
|
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">Previous</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<a class="page-link" href="#">{{ page_obj.number }}</a>
|
<a class="page-link" href="#">{{ page_obj.number }}</a>
|
||||||
|
|
||||||
{% if page_obj.has_next %}
|
{% if page_obj.has_next %}
|
||||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}">Next</a>
|
<a class="page-link" href="?page={{ page_obj.next_page_number }}">Next</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
@@ -1,20 +1,88 @@
|
|||||||
|
|
||||||
<article class="card">
|
<article class="card">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="avatar-box">avatar</div>
|
|
||||||
|
<div class="avatar-box">
|
||||||
|
|
||||||
|
{% if question.user.profile.avatar %}
|
||||||
|
<img src="{{ question.user.profile.avatar.url }}" width="80">
|
||||||
|
{% else %}
|
||||||
|
avatar
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="vote-box">
|
<div class="vote-box">
|
||||||
<span>5</span>
|
|
||||||
<span class="vote-arrows">^</span>
|
<span
|
||||||
|
class="question-rating"
|
||||||
|
id="question-rating-{{ question.id }}"
|
||||||
|
>
|
||||||
|
{{ question.rating }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
|
||||||
|
{% if question.id not in user_reactions %}
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="question-like"
|
||||||
|
data-id="{{ question.id }}"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="question-dislike"
|
||||||
|
data-id="{{ question.id }}"
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
<span>voted</span>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="question-title" href="question">{{ question.title }}</p>
|
|
||||||
<p>{{ question.text }}</p>
|
<a
|
||||||
|
class="question-title"
|
||||||
|
href="/question?question_id={{ question.id }}"
|
||||||
|
>
|
||||||
|
{{ question.title }}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<p>{{ question.body }}</p>
|
||||||
|
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<a href="question">answer (3)</a>
|
|
||||||
|
<a href="/question?question_id={{ question.id }}">
|
||||||
|
answer
|
||||||
|
</a>
|
||||||
|
|
||||||
<span>Tags:</span>
|
<span>Tags:</span>
|
||||||
<a class="tag" href="#">black-jack</a>
|
|
||||||
<a class="tag" href="#">bender</a>
|
{% for tag in question.tags.all %}
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="tag"
|
||||||
|
href="/?tag={{ tag }}"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</article>
|
|
||||||
|
</article>
|
||||||
@@ -6,30 +6,37 @@
|
|||||||
<h1>Profile: Dr. Pepper</h1>
|
<h1>Profile: Dr. Pepper</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="form">
|
<form class="form" method="post" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="login">Login</label>
|
<label>Email</label>
|
||||||
<input id="login" type="text" value="dr_pepper">
|
{{ form.email }}
|
||||||
|
{{ form.email.errors }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="email">Email</label>
|
<label>Nickname</label>
|
||||||
<input id="email" type="email" value="drpepper@mail.ru">
|
{{ form.nickname }}
|
||||||
|
{{ form.nickname.errors }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="nickname">NickName</label>
|
<label>Avatar</label>
|
||||||
<input id="nickname" type="text" value="Dr. Pepper">
|
{{ form.avatar }}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
|
||||||
<label>Upload avatar</label>
|
<div class="avatar-preview">
|
||||||
<div class="avatar-box">avatar</div>
|
{% if request.user.profile.avatar %}
|
||||||
</div>
|
<img src="{{ request.user.profile.avatar.url }}" width="120">
|
||||||
<div class="form-row">
|
{% else %}
|
||||||
<span></span>
|
<img src="{% static 'core/img/profile.webp' %}" width="120">
|
||||||
<input type="file">
|
{% endif %}
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button class="button" type="button">Save</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button class="button" type="submit">Save</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -4,63 +4,187 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<article class="card">
|
<article class="card">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="avatar-box">avatar</div>
|
|
||||||
<div class="vote-box">
|
<div class="avatar-box">
|
||||||
<span>5</span>
|
|
||||||
<span class="vote-arrows">^<span>'</span></span>
|
{% if question.user.profile.avatar %}
|
||||||
|
<img src="{{ question.user.profile.avatar.url }}" width="80">
|
||||||
|
{% else %}
|
||||||
|
avatar
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="vote-box">
|
||||||
|
|
||||||
|
<span id="question-rating-{{ question.id }}">
|
||||||
|
{{ question.rating }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
|
||||||
|
{% if not question_reaction %}
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="question-like"
|
||||||
|
data-id="{{ question.id }}"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="question-dislike"
|
||||||
|
data-id="{{ question.id }}"
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
<span>voted</span>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="question-title">How to build a moon park ?</div>
|
|
||||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.</p>
|
<div class="question-title">
|
||||||
|
{{ question.title }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>{{ question.body }}</p>
|
||||||
|
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
|
|
||||||
<span>Tags:</span>
|
<span>Tags:</span>
|
||||||
<a class="tag" href="#">black-jack</a>
|
|
||||||
<a class="tag" href="#">bender</a>
|
{% for tag in question.tags.all %}
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="tag"
|
||||||
|
href="/?tag={{ tag }}"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="answer-card">
|
|
||||||
|
{% for answer in answers %}
|
||||||
|
|
||||||
|
<article
|
||||||
|
class="answer-card"
|
||||||
|
id="answer-{{ answer.id }}"
|
||||||
|
>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="avatar-box">avatar</div>
|
|
||||||
|
<div class="avatar-box">
|
||||||
|
|
||||||
|
{% if answer.user.profile.avatar %}
|
||||||
|
<img src="{{ answer.user.profile.avatar.url }}" width="80">
|
||||||
|
{% else %}
|
||||||
|
avatar
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="vote-box">
|
<div class="vote-box">
|
||||||
<span>5</span>
|
|
||||||
<span class="vote-arrows">^<span>'</span></span>
|
<span id="answer-rating-{{ answer.id }}">
|
||||||
|
{{ answer.rating }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
|
||||||
|
{% if answer.id not in answer_reactions %}
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="answer-like"
|
||||||
|
data-id="{{ answer.id }}"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="answer-dislike"
|
||||||
|
data-id="{{ answer.id }}"
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
<span>voted</span>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p>First of all I would like to thank you for the invitation to participate in such a ... Russia is the huge territory which in many respects needs to be render habitable.</p>
|
|
||||||
|
<p>{{ answer.body }}</p>
|
||||||
|
|
||||||
<div class="answer-controls">
|
<div class="answer-controls">
|
||||||
<input type="checkbox" checked>
|
|
||||||
<span>Correct!</span>
|
{% if question.correct_answer_id == answer.id %}
|
||||||
|
|
||||||
|
<span class="correct-label">
|
||||||
|
✔ Correct
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if request.user == question.user %}
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="correct-answer-btn"
|
||||||
|
data-question="{{ question.id }}"
|
||||||
|
data-answer="{{ answer.id }}"
|
||||||
|
>
|
||||||
|
Correct
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="answer-card">
|
{% endfor %}
|
||||||
<div>
|
|
||||||
<div class="avatar-box">avatar</div>
|
|
||||||
<div class="vote-box">
|
<form method="post">
|
||||||
<span>3</span>
|
|
||||||
<span class="vote-arrows">^<span>'</span></span>
|
{% csrf_token %}
|
||||||
</div>
|
|
||||||
</div>
|
{{ form.body }}
|
||||||
<div>
|
|
||||||
<p>First of all I would like to thank you for the invitation to participate in such a ... Russia is the huge territory which in many respects needs to be render habitable.</p>
|
{{ form.body.errors }}
|
||||||
<div class="answer-controls">
|
|
||||||
<input type="checkbox">
|
<button type="submit">
|
||||||
<span>Correct!</span>
|
Answer
|
||||||
</div>
|
</button>
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<form class="form">
|
|
||||||
<label for="answer">Enter your answer here..</label>
|
|
||||||
<textarea id="answer"></textarea>
|
|
||||||
<button class="button" type="button">Answer</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -7,38 +7,51 @@
|
|||||||
<h1>Create Account</h1>
|
<h1>Create Account</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="error">Sorry, this email address already registered!</p>
|
<form class="form" method="post" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
|
||||||
<form class="form">
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="login">Login</label>
|
<label>Login</label>
|
||||||
<input id="login" type="text" value="dr_pepper">
|
{{ form.username }}
|
||||||
|
{{ form.username.errors }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="email">Email</label>
|
<label>Email</label>
|
||||||
<input id="email" type="email" value="drpepper@mail.ru">
|
{{ form.email }}
|
||||||
|
{{ form.email.errors }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="nickname">NickName</label>
|
<label>NickName</label>
|
||||||
<input id="nickname" type="text" value="Dr. Pepper">
|
{{ form.nickname }}
|
||||||
|
{{ form.nickname.errors }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="password">Password</label>
|
<label>Password</label>
|
||||||
<input id="password" type="password" value="********">
|
{{ form.password }}
|
||||||
|
{{ form.password.errors }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="repeat">Repeat password</label>
|
<label>Repeat password</label>
|
||||||
<input id="repeat" type="password" value="********">
|
{{ form.password_repeat }}
|
||||||
|
{{ form.password_repeat.errors }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label>Upload avatar</label>
|
<label>Avatar</label>
|
||||||
<input type="file">
|
{{ form.avatar }}
|
||||||
|
{{ form.avatar.errors }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button class="button" type="button">Register!</button>
|
<button class="button" type="submit">Register!</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+11
-1
@@ -1,4 +1,6 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
|
||||||
from questions import views
|
from questions import views
|
||||||
|
|
||||||
@@ -9,4 +11,12 @@ urlpatterns = [
|
|||||||
path('profile', views.profile, name='profile'),
|
path('profile', views.profile, name='profile'),
|
||||||
path('question', views.question, name='question'),
|
path('question', views.question, name='question'),
|
||||||
path('signup', views.signup, name='signup'),
|
path('signup', views.signup, name='signup'),
|
||||||
]
|
path('hot', views.hot, name='hot'),
|
||||||
|
path('logout', views.logout, name='logout'),
|
||||||
|
path('ajax/question/react/', views.question_react),
|
||||||
|
path('ajax/answer/react/', views.answer_react),
|
||||||
|
path('ajax/answer/correct/', views.mark_correct),
|
||||||
|
]
|
||||||
|
|
||||||
|
if settings.DEBUG:
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
|
def crop_square(image_path, size=(300, 300)):
|
||||||
|
img = Image.open(image_path)
|
||||||
|
|
||||||
|
width, height = img.size
|
||||||
|
|
||||||
|
min_side = min(width, height)
|
||||||
|
|
||||||
|
left = (width - min_side) / 2
|
||||||
|
top = (height - min_side) / 2
|
||||||
|
right = (width + min_side) / 2
|
||||||
|
bottom = (height + min_side) / 2
|
||||||
|
|
||||||
|
img = img.crop((left, top, right, bottom))
|
||||||
|
|
||||||
|
img = img.resize(size)
|
||||||
|
|
||||||
|
img.save(image_path)
|
||||||
+409
-24
@@ -1,38 +1,423 @@
|
|||||||
from django.shortcuts import render
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
|
from django.contrib.auth import login as auth_login, logout as auth_logout
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.utils.http import url_has_allowed_host_and_scheme
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.views.decorators.http import require_POST
|
||||||
|
|
||||||
|
from .models import (
|
||||||
|
Question,
|
||||||
|
Tag,
|
||||||
|
Answer,
|
||||||
|
QuestionReaction,
|
||||||
|
AnswerReaction
|
||||||
|
)
|
||||||
|
|
||||||
|
from .forms import (
|
||||||
|
LoginForm,
|
||||||
|
SignupForm,
|
||||||
|
ProfileForm,
|
||||||
|
AskForm,
|
||||||
|
AnswerForm
|
||||||
|
)
|
||||||
|
|
||||||
QUESTIONS = [
|
|
||||||
{
|
|
||||||
"id": i,
|
|
||||||
"title": f'Question number {i}',
|
|
||||||
"text": f'Some text {i}',
|
|
||||||
}
|
|
||||||
for i in range(10)
|
|
||||||
]
|
|
||||||
|
|
||||||
def index(request):
|
def index(request):
|
||||||
try:
|
|
||||||
page_number = int(request.GET.get('page', 1))
|
tag_name = request.GET.get('tag')
|
||||||
except ValueError:
|
|
||||||
page_number = 1
|
questions = Question.objects.all() \
|
||||||
page = Paginator(QUESTIONS, 4)
|
.select_related('user') \
|
||||||
page_obj = page.page(page_number)
|
.prefetch_related('tags') \
|
||||||
return render(request, 'core/index.html', context={'questions': page_obj.object_list, 'page_obj': page_obj})
|
.order_by('-created_at')
|
||||||
|
|
||||||
|
if tag_name:
|
||||||
|
questions = questions.filter(tags__name=tag_name)
|
||||||
|
|
||||||
|
paginator = Paginator(questions, 4)
|
||||||
|
|
||||||
|
page_number = request.GET.get('page', 1)
|
||||||
|
|
||||||
|
page_obj = paginator.get_page(page_number)
|
||||||
|
|
||||||
|
user_reactions = {}
|
||||||
|
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
|
||||||
|
reactions = QuestionReaction.objects.filter(
|
||||||
|
user=request.user,
|
||||||
|
question__in=page_obj.object_list
|
||||||
|
)
|
||||||
|
|
||||||
|
user_reactions = {
|
||||||
|
reaction.question_id: reaction.positive
|
||||||
|
for reaction in reactions
|
||||||
|
}
|
||||||
|
|
||||||
|
hot_questions = Question.objects.order_by('-rating')[:4]
|
||||||
|
|
||||||
|
return render(request, 'core/index.html', {
|
||||||
|
'questions': page_obj.object_list,
|
||||||
|
'page_obj': page_obj,
|
||||||
|
'hot_questions': hot_questions,
|
||||||
|
'tags': Tag.objects.all(),
|
||||||
|
'tag': tag_name,
|
||||||
|
'user_reactions': user_reactions
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def ask(request):
|
def hot(request):
|
||||||
return render(request, 'core/ask.html', context={'questions': QUESTIONS})
|
|
||||||
|
questions = Question.objects.all() \
|
||||||
|
.select_related('user') \
|
||||||
|
.prefetch_related('tags') \
|
||||||
|
.order_by('-rating')
|
||||||
|
|
||||||
|
paginator = Paginator(questions, 4)
|
||||||
|
|
||||||
|
page_number = request.GET.get('page', 1)
|
||||||
|
|
||||||
|
page_obj = paginator.get_page(page_number)
|
||||||
|
|
||||||
|
user_reactions = {}
|
||||||
|
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
|
||||||
|
reactions = QuestionReaction.objects.filter(
|
||||||
|
user=request.user,
|
||||||
|
question__in=page_obj.object_list
|
||||||
|
)
|
||||||
|
|
||||||
|
user_reactions = {
|
||||||
|
reaction.question_id: reaction.positive
|
||||||
|
for reaction in reactions
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, 'core/hot.html', {
|
||||||
|
'questions': page_obj.object_list,
|
||||||
|
'page_obj': page_obj,
|
||||||
|
'hot_questions': questions[:4],
|
||||||
|
'tags': Tag.objects.all(),
|
||||||
|
'user_reactions': user_reactions
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def question(request):
|
||||||
|
|
||||||
|
question_id = request.GET.get('question_id')
|
||||||
|
|
||||||
|
question = get_object_or_404(
|
||||||
|
Question.objects.select_related('user')
|
||||||
|
.prefetch_related('tags', 'answers'),
|
||||||
|
id=question_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
return redirect(
|
||||||
|
f'/login?next=/question?question_id={question_id}'
|
||||||
|
)
|
||||||
|
|
||||||
|
form = AnswerForm(request.POST)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
|
||||||
|
answer = form.save(
|
||||||
|
user=request.user,
|
||||||
|
question=question
|
||||||
|
)
|
||||||
|
|
||||||
|
return redirect(
|
||||||
|
f'/question?question_id={question.id}#answer-{answer.id}'
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
form = AnswerForm()
|
||||||
|
|
||||||
|
question_reaction = None
|
||||||
|
|
||||||
|
answer_reactions = {}
|
||||||
|
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
|
||||||
|
question_reaction = QuestionReaction.objects.filter(
|
||||||
|
question=question,
|
||||||
|
user=request.user
|
||||||
|
).first()
|
||||||
|
|
||||||
|
reactions = AnswerReaction.objects.filter(
|
||||||
|
user=request.user,
|
||||||
|
answer__question=question
|
||||||
|
)
|
||||||
|
|
||||||
|
answer_reactions = {
|
||||||
|
reaction.answer_id: reaction.positive
|
||||||
|
for reaction in reactions
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, 'core/question.html', {
|
||||||
|
'question': question,
|
||||||
|
'answers': question.answers.all(),
|
||||||
|
'form': form,
|
||||||
|
'tags': Tag.objects.all(),
|
||||||
|
'question_reaction': question_reaction,
|
||||||
|
'answer_reactions': answer_reactions
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def login(request):
|
def login(request):
|
||||||
return render(request, 'core/login.html', context={'questions': QUESTIONS})
|
|
||||||
|
|
||||||
def profile(request):
|
next_url = request.GET.get('next', '/')
|
||||||
return render(request, 'core/profile.html', context={'questions': QUESTIONS})
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
|
||||||
|
form = LoginForm(request.POST)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
|
||||||
|
user = form.cleaned_data['user']
|
||||||
|
|
||||||
|
auth_login(request, user)
|
||||||
|
|
||||||
|
if not url_has_allowed_host_and_scheme(
|
||||||
|
next_url,
|
||||||
|
allowed_hosts={request.get_host()}
|
||||||
|
):
|
||||||
|
next_url = '/'
|
||||||
|
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
form = LoginForm()
|
||||||
|
|
||||||
|
return render(request, 'core/login.html', {
|
||||||
|
'form': form,
|
||||||
|
'next': next_url,
|
||||||
|
'tags': Tag.objects.all()
|
||||||
|
})
|
||||||
|
|
||||||
def question(request):
|
|
||||||
return render(request, 'core/question.html', context={'questions': QUESTIONS})
|
|
||||||
|
|
||||||
def signup(request):
|
def signup(request):
|
||||||
return render(request, 'core/signup.html', context={'questions': QUESTIONS})
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
|
||||||
|
form = SignupForm(request.POST, request.FILES)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
|
||||||
|
user = form.save()
|
||||||
|
|
||||||
|
auth_login(request, user)
|
||||||
|
|
||||||
|
return redirect('/')
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
form = SignupForm()
|
||||||
|
|
||||||
|
return render(request, 'core/signup.html', {
|
||||||
|
'form': form,
|
||||||
|
'tags': Tag.objects.all()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def logout(request):
|
||||||
|
|
||||||
|
auth_logout(request)
|
||||||
|
|
||||||
|
return redirect(
|
||||||
|
request.META.get('HTTP_REFERER', '/')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def profile(request):
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
|
||||||
|
form = ProfileForm(
|
||||||
|
request.POST,
|
||||||
|
request.FILES,
|
||||||
|
user=request.user
|
||||||
|
)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
|
||||||
|
form.save()
|
||||||
|
|
||||||
|
return redirect('profile')
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
form = ProfileForm(user=request.user)
|
||||||
|
|
||||||
|
return render(request, 'core/profile.html', {
|
||||||
|
'form': form,
|
||||||
|
'tags': Tag.objects.all()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def ask(request):
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
|
||||||
|
form = AskForm(request.POST)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
|
||||||
|
question = form.save(user=request.user)
|
||||||
|
|
||||||
|
return redirect(
|
||||||
|
f'/question?question_id={question.id}'
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
form = AskForm()
|
||||||
|
|
||||||
|
return render(request, 'core/ask.html', {
|
||||||
|
'form': form,
|
||||||
|
'tags': Tag.objects.all()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
@login_required
|
||||||
|
def question_react(request):
|
||||||
|
|
||||||
|
question_id = request.POST.get('question_id')
|
||||||
|
|
||||||
|
reaction_type = request.POST.get('type')
|
||||||
|
|
||||||
|
if reaction_type not in ['like', 'dislike']:
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'error': 'invalid type'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
question = get_object_or_404(
|
||||||
|
Question,
|
||||||
|
id=question_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if QuestionReaction.objects.filter(
|
||||||
|
question=question,
|
||||||
|
user=request.user
|
||||||
|
).exists():
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'error': 'already reacted'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
positive = reaction_type == 'like'
|
||||||
|
|
||||||
|
QuestionReaction.objects.create(
|
||||||
|
question=question,
|
||||||
|
user=request.user,
|
||||||
|
positive=positive
|
||||||
|
)
|
||||||
|
|
||||||
|
if positive:
|
||||||
|
question.rating += 1
|
||||||
|
else:
|
||||||
|
question.rating -= 1
|
||||||
|
|
||||||
|
question.save()
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'rating': question.rating
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
@login_required
|
||||||
|
def answer_react(request):
|
||||||
|
|
||||||
|
answer_id = request.POST.get('answer_id')
|
||||||
|
|
||||||
|
reaction_type = request.POST.get('type')
|
||||||
|
|
||||||
|
if reaction_type not in ['like', 'dislike']:
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'error': 'invalid type'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
answer = get_object_or_404(
|
||||||
|
Answer,
|
||||||
|
id=answer_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if AnswerReaction.objects.filter(
|
||||||
|
answer=answer,
|
||||||
|
user=request.user
|
||||||
|
).exists():
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'error': 'already reacted'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
positive = reaction_type == 'like'
|
||||||
|
|
||||||
|
AnswerReaction.objects.create(
|
||||||
|
answer=answer,
|
||||||
|
user=request.user,
|
||||||
|
positive=positive
|
||||||
|
)
|
||||||
|
|
||||||
|
if positive:
|
||||||
|
answer.rating += 1
|
||||||
|
else:
|
||||||
|
answer.rating -= 1
|
||||||
|
|
||||||
|
answer.save()
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'rating': answer.rating
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
@login_required
|
||||||
|
def mark_correct(request):
|
||||||
|
|
||||||
|
question_id = request.POST.get('question_id')
|
||||||
|
|
||||||
|
answer_id = request.POST.get('answer_id')
|
||||||
|
|
||||||
|
question = get_object_or_404(
|
||||||
|
Question,
|
||||||
|
id=question_id
|
||||||
|
)
|
||||||
|
|
||||||
|
answer = get_object_or_404(
|
||||||
|
Answer,
|
||||||
|
id=answer_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if question.user != request.user:
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'error': 'forbidden'
|
||||||
|
}, status=403)
|
||||||
|
|
||||||
|
if answer.question != question:
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'error': 'wrong answer'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
question.correct_answer = answer
|
||||||
|
|
||||||
|
question.save()
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'success': True,
|
||||||
|
'correct_answer_id': answer.id
|
||||||
|
})
|
||||||
+3
-1
@@ -1,2 +1,4 @@
|
|||||||
Django==5.2.12
|
Django==5.2.12
|
||||||
python-dotenv==1.2.2
|
python-dotenv==1.2.2
|
||||||
|
pillow==12.2.0
|
||||||
|
psycopg2-binary==2.9.12
|
||||||
Reference in new issue
Block a user