482 lines
10 KiB
Python
482 lines
10 KiB
Python
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 django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector
|
|
|
|
from .models import (
|
|
Question,
|
|
Tag,
|
|
Answer,
|
|
QuestionReaction,
|
|
AnswerReaction
|
|
)
|
|
|
|
from .forms import (
|
|
LoginForm,
|
|
SignupForm,
|
|
ProfileForm,
|
|
AskForm,
|
|
AnswerForm
|
|
)
|
|
from . import tasks as tasks_module
|
|
|
|
|
|
def index(request):
|
|
|
|
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 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 search(request):
|
|
query_text = request.GET.get('q', '').strip()
|
|
|
|
if not query_text:
|
|
return JsonResponse({'results': []})
|
|
|
|
vector = (
|
|
SearchVector('title', weight='A') +
|
|
SearchVector('body', weight='B')
|
|
)
|
|
query = SearchQuery(query_text, search_type='websearch')
|
|
questions = Question.objects.annotate(
|
|
rank=SearchRank(vector, query)
|
|
).filter(rank__gt=0).order_by('-rank')[:10]
|
|
|
|
results = [
|
|
{
|
|
'id': q.id,
|
|
'title': q.title,
|
|
'url': f'/question?question_id={q.id}'
|
|
}
|
|
for q in questions
|
|
]
|
|
|
|
return JsonResponse({'results': results})
|
|
|
|
|
|
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
|
|
)
|
|
|
|
# trigger async notifications and cache updates
|
|
try:
|
|
payload = {
|
|
'id': answer.id,
|
|
'body': answer.body,
|
|
'user': answer.user.username
|
|
}
|
|
tasks_module.publish_new_answer_centrifugo.delay(question.id, payload)
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
# send email to question author
|
|
recipient = []
|
|
if hasattr(question.user, 'profile'):
|
|
recipient = [question.user.profile.email]
|
|
subject = f'New answer to your question: {question.title}'
|
|
message = f'User {answer.user.username} answered your question.\n\n{answer.body}'
|
|
if recipient and recipient[0]:
|
|
tasks_module.send_new_answer_email.delay(subject, message, recipient)
|
|
except Exception:
|
|
pass
|
|
|
|
# optionally trigger cache rebuilds
|
|
try:
|
|
tasks_module.rebuild_popular_tags.delay()
|
|
tasks_module.rebuild_best_members.delay()
|
|
except Exception:
|
|
pass
|
|
|
|
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):
|
|
|
|
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 signup(request):
|
|
|
|
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
|
|
}) |