3 Commits
Author SHA1 Message Date
redtea becfbeb070 Update README.md 2026-05-10 23:02:24 +03:00
redtea 466555ce05 hw3-5
hw3
- Введены типы данных в models.py, сайт переписан под django модели
- DB переведена на PostgreSQL
- Переписан docker compose файл, настроены network и volume
- Добавлена команда генерации рандомных данных в db
hw4
- Добавлены формы login, signup, logout, profile
- Добавлены вопросы и ответы и их модели и формы
hw5
- Добавлена загрузка и отображение аватарок
- Добавлены лайки и дизайки, отметка правильного ответа
2026-05-10 22:56:49 +03:00
redtea 33379408ba hw-2 fix
- На лого слева вверху добавлена ссылка на главную
- Добавлен механизм отображения вопросов по тегу
- Теперь теги берутся из списка в контроллере
- Теперь при нажатии на тег открывается страница вопросов по тегу
- Добавлена страница hot questions
- При нажатии на название вопроса теперь открывается страница вопроса
- Улучшена пагинация
2026-04-09 22:35:56 +03:00
31 changed files with 1774 additions and 317 deletions

No files matched your search

+13
View File
@@ -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"
+7
View File
@@ -1,2 +1,9 @@
SECRET_KEY=django-insecure-1234
DEBUG=True
DB_NAME=vkdesk
DB_USER=postgres
DB_PASSWORD=1234567
DB_HOST=localhost
DB_PORT=5432
DB_ENGINE="django.db.backends.postgresql"
+6 -7
View File
@@ -1,15 +1,14 @@
FROM python:3.10-slim
FROM python:3.11
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY ./requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY requirements.txt .
RUN pip install --upgrade pip
RUN pip install -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
+16 -18
View File
@@ -10,31 +10,19 @@
🔧 Студент 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
pip install --no-cache-dir -r requirements.txt
python manage.py runserver 8000
pip install --no-cache-dir -r requirements.txt
```
### 2. Docker
### Docker
ВАЖНО! Использовать .env.docker вместо .env.example в качестве образца!
```shell
docker compose up --build
@@ -42,3 +30,13 @@ docker compose up --build
Запустится именно на 127.0.0.1:8000
## Миграции и заполнение дб
Заполнить ДБ 10000 рандомных данных
```
python manage.py makemigrations
python manage.py migrate
python manage.py fill_db 10000
```
+12 -3
View File
@@ -38,6 +38,7 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'questions',
]
MIDDLEWARE = [
@@ -80,8 +81,12 @@ WSGI_APPLICATION = 'application.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
'ENGINE': os.getenv('DB_ENGINE'),
'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
# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = 'ru-RU'
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
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
+60 -1
View File
@@ -1,11 +1,70 @@
version: '3.8'
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:
build: .
container_name: django-app-container
restart: always
ports:
- "8000:8000"
working_dir: /app
volumes:
- .:/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:
+173
View File
@@ -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
View File
Whitespace-only changes.
Whitespace-only changes.
+189
View File
@@ -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'))
+68
View File
@@ -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
View File
@@ -1,3 +1,160 @@
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')
+19
View File
@@ -204,6 +204,10 @@ a:hover {
box-shadow: 0.25rem 0.25rem 0 rgba(31, 45, 58, 0.08);
}
.not-underlined {
text-decoration: none;
}
.sidebar h2 {
margin: 0 0 0.5rem;
font-size: 1.25rem;
@@ -307,6 +311,21 @@ a:hover {
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 {
border-top: 0.125rem solid #1f2d3a;
padding: 0.6rem 1.1rem;
+182
View File
@@ -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');
}
}
});
});
+17 -10
View File
@@ -6,22 +6,29 @@
<div class="main-title">
<h1>Ask a Question</h1>
</div>
<form class="form" method="post">
{% csrf_token %}
{{ form.non_field_errors }}
<form class="form">
<div class="form-row">
<label for="title">Title</label>
<input id="title" type="text" value="How to build a moon park ?">
<label>Title</label>
{{ form.title }}
{{ form.title.errors }}
</div>
<div class="form-row">
<label for="text">Text</label>
<textarea id="text">Really, how ? Have no idea about it</textarea>
<label>Text</label>
{{ form.body }}
{{ form.body.errors }}
</div>
<div class="form-row">
<label for="tags">Tags</label>
<input id="tags" type="text" value="moon, park, puzzle">
</div>
<div>
<button class="button" type="button">ASK IT</button>
<label>Tags</label>
{{ form.tags }}
{{ form.tags.errors }}
</div>
<button class="button" type="submit">ASK IT</button>
</form>
{% endblock %}
-140
View File
@@ -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>
+16
View File
@@ -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 %}
+7 -2
View File
@@ -3,10 +3,15 @@
{% block content %}
<div class="main-title">
<h1>Fresh Questions</h1>
<a href="#">Top Today</a>
<h1>All Questions</h1>
<a href="/hot">Hot questions</a>
</div>
{% if tag %}
<div class="current-tag">
Текущий тег: <span>#{{ tag }}</span>
</div>
{% endif %}
{% for question in questions %}
{% include 'core/partials/question.html' %}
{% endfor %}
+24 -24
View File
@@ -6,22 +6,37 @@
<title>VkDesk | Fresh Questions</title>
<meta name="description" content="VkDesk Q&A">
<link rel="stylesheet" href="{% static 'core/css/style.css' %}">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<header class="site-header">
<div class="logo">VkDesk</div>
<a href="/" class="logo not-underlined">VkDesk</a>
<form class="search">
<input type="text" placeholder="Search">
<a class="button button--ask" href="ask">ASK IT</a>
</form>
<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">
<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>
<a href="profile">settings</a>
<a href="login">log out</a>
<a href="{% url 'logout' %}">log out</a>
</div>
{% else %}
<div>
<a href="{% url 'login' %}">log in</a>
<a href="{% url 'signup' %}">sign up</a>
</div>
{% endif %}
</div>
</div>
</header>
@@ -34,31 +49,16 @@
<div class="sidebar-section">
<h2>Trending Tags</h2>
<div class="tag-cloud">
<span>perl</span>
<span class="tag-red">python</span>
<span>TechnoPark</span>
<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>
{% for tag in tags %}
<a href="/?tag={{ tag }}" class="not-underlined">{{tag}}</a>
{% endfor %}
</div>
</div>
</aside>
</div>
<footer class="footer">2026</footer>
</body>
<script src="{% static 'core/js/script.js' %}"></script>
</html>
+18 -8
View File
@@ -8,21 +8,31 @@
<h1>Sign In</h1>
</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">
<label for="login">Login</label>
<input id="login" type="text" placeholder="Enter your login here">
<label>Login</label>
{{ form.username }}
{{ form.username.errors }}
</div>
<div class="form-row">
<label for="password">Password</label>
<input id="password" type="password" value="********">
<label>Password</label>
{{ form.password }}
{{ form.password.errors }}
</div>
<input type="hidden" name="next" value="{{ next }}">
<div>
<button class="button" type="button">Sign In!</button>
<button class="button" type="submit">Sign In!</button>
</div>
<a href="signup">create new account</a>
<a href="{% url 'signup' %}">create new account</a>
</form>
@@ -1,9 +1,13 @@
<nav class="pagination">
{% if page_obj.paginator.num_pages > 1 %}
{% 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 %}
<a class="page-link" href="#">{{ page_obj.number }}</a>
{% if page_obj.has_next %}
<a class="page-link" href="?page={{ page_obj.next_page_number }}">Next</a>
{% endif %}
{% endif %}
</nav>
@@ -1,20 +1,88 @@
<article class="card">
<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">
<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>
<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">
<a href="question">answer (3)</a>
<a href="/question?question_id={{ question.id }}">
answer
</a>
<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>
</article>
+24 -17
View File
@@ -6,30 +6,37 @@
<h1>Profile: Dr. Pepper</h1>
</div>
<form class="form">
<form class="form" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.non_field_errors }}
<div class="form-row">
<label for="login">Login</label>
<input id="login" type="text" value="dr_pepper">
<label>Email</label>
{{ form.email }}
{{ form.email.errors }}
</div>
<div class="form-row">
<label for="email">Email</label>
<input id="email" type="email" value="drpepper@mail.ru">
<label>Nickname</label>
{{ form.nickname }}
{{ form.nickname.errors }}
</div>
<div class="form-row">
<label for="nickname">NickName</label>
<input id="nickname" type="text" value="Dr. Pepper">
<label>Avatar</label>
{{ form.avatar }}
</div>
<div class="form-row">
<label>Upload avatar</label>
<div class="avatar-box">avatar</div>
</div>
<div class="form-row">
<span></span>
<input type="file">
</div>
<div>
<button class="button" type="button">Save</button>
<div class="avatar-preview">
{% if request.user.profile.avatar %}
<img src="{{ request.user.profile.avatar.url }}" width="120">
{% else %}
<img src="{% static 'core/img/profile.webp' %}" width="120">
{% endif %}
</div>
<button class="button" type="submit">Save</button>
</form>
{% endblock %}
+160 -36
View File
@@ -4,63 +4,187 @@
{% block content %}
<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 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">
<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 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">
<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>
</article>
<article class="answer-card">
{% for answer in answers %}
<article
class="answer-card"
id="answer-{{ answer.id }}"
>
<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">
<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>
<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">
<input type="checkbox" checked>
<span>Correct!</span>
{% if question.correct_answer_id == answer.id %}
<span class="correct-label">
✔ Correct
</span>
{% endif %}
</div>
{% if request.user == question.user %}
<button
class="correct-answer-btn"
data-question="{{ question.id }}"
data-answer="{{ answer.id }}"
>
Correct
</button>
{% endif %}
</div>
</article>
<article class="answer-card">
<div>
<div class="avatar-box">avatar</div>
<div class="vote-box">
<span>3</span>
<span class="vote-arrows">^<span>'</span></span>
</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>
<div class="answer-controls">
<input type="checkbox">
<span>Correct!</span>
</div>
</div>
</article>
{% endfor %}
<form method="post">
{% csrf_token %}
{{ form.body }}
{{ form.body.errors }}
<button type="submit">
Answer
</button>
<form class="form">
<label for="answer">Enter your answer here..</label>
<textarea id="answer"></textarea>
<button class="button" type="button">Answer</button>
</form>
{% endblock %}
+29 -16
View File
@@ -7,38 +7,51 @@
<h1>Create Account</h1>
</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">
<label for="login">Login</label>
<input id="login" type="text" value="dr_pepper">
<label>Login</label>
{{ form.username }}
{{ form.username.errors }}
</div>
<div class="form-row">
<label for="email">Email</label>
<input id="email" type="email" value="drpepper@mail.ru">
<label>Email</label>
{{ form.email }}
{{ form.email.errors }}
</div>
<div class="form-row">
<label for="nickname">NickName</label>
<input id="nickname" type="text" value="Dr. Pepper">
<label>NickName</label>
{{ form.nickname }}
{{ form.nickname.errors }}
</div>
<div class="form-row">
<label for="password">Password</label>
<input id="password" type="password" value="********">
<label>Password</label>
{{ form.password }}
{{ form.password.errors }}
</div>
<div class="form-row">
<label for="repeat">Repeat password</label>
<input id="repeat" type="password" value="********">
<label>Repeat password</label>
{{ form.password_repeat }}
{{ form.password_repeat.errors }}
</div>
<div class="form-row">
<label>Upload avatar</label>
<input type="file">
<label>Avatar</label>
{{ form.avatar }}
{{ form.avatar.errors }}
</div>
<div>
<button class="button" type="button">Register!</button>
<button class="button" type="submit">Register!</button>
</div>
</form>
{% endblock %}
+10
View File
@@ -1,4 +1,6 @@
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
from questions import views
@@ -9,4 +11,12 @@ urlpatterns = [
path('profile', views.profile, name='profile'),
path('question', views.question, name='question'),
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)
+20
View File
@@ -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
View File
@@ -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.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):
try:
page_number = int(request.GET.get('page', 1))
except ValueError:
page_number = 1
page = Paginator(QUESTIONS, 4)
page_obj = page.page(page_number)
return render(request, 'core/index.html', context={'questions': page_obj.object_list, 'page_obj': page_obj})
tag_name = request.GET.get('tag')
questions = Question.objects.all() \
.select_related('user') \
.prefetch_related('tags') \
.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):
return render(request, 'core/ask.html', context={'questions': QUESTIONS})
def hot(request):
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):
return render(request, 'core/login.html', context={'questions': QUESTIONS})
def profile(request):
return render(request, 'core/profile.html', context={'questions': QUESTIONS})
next_url = request.GET.get('next', '/')
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):
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
})
+2
View File
@@ -1,2 +1,4 @@
Django==5.2.12
python-dotenv==1.2.2
pillow==12.2.0
psycopg2-binary==2.9.12