457 lines
16 KiB
Python
457 lines
16 KiB
Python
from django.db.models import Q
|
|
from django.http import HttpResponseBadRequest, HttpResponseNotAllowed
|
|
from django.shortcuts import render, redirect, get_object_or_404
|
|
from django.views.decorators.http import require_POST
|
|
|
|
from django.contrib.auth.decorators import login_required
|
|
|
|
from symposion.conf import settings
|
|
from symposion.proposals.models import ProposalBase, ProposalSection
|
|
from symposion.teams.models import Team
|
|
from symposion.utils.mail import send_email
|
|
|
|
from symposion.reviews.forms import ReviewForm, SpeakerCommentForm
|
|
from symposion.reviews.forms import BulkPresentationForm
|
|
from symposion.reviews.models import (
|
|
ReviewAssignment, Review, LatestVote, ProposalResult, NotificationTemplate
|
|
)
|
|
|
|
|
|
def access_not_permitted(request):
|
|
return render(request, "reviews/access_not_permitted.html")
|
|
|
|
|
|
def proposals_generator(request, queryset, user_pk=None, check_speaker=True):
|
|
|
|
for obj in queryset:
|
|
# @@@ this sucks; we can do better
|
|
if check_speaker:
|
|
if request.user in [s.user for s in obj.speakers()]:
|
|
continue
|
|
|
|
try:
|
|
obj.result
|
|
except ProposalResult.DoesNotExist:
|
|
ProposalResult.objects.get_or_create(proposal=obj)
|
|
|
|
obj.comment_count = obj.result.comment_count
|
|
obj.total_votes = obj.result.vote_count
|
|
obj.plus_one = obj.result.plus_one
|
|
obj.plus_zero = obj.result.plus_zero
|
|
obj.minus_zero = obj.result.minus_zero
|
|
obj.minus_one = obj.result.minus_one
|
|
lookup_params = dict(proposal=obj)
|
|
|
|
if user_pk:
|
|
lookup_params["user__pk"] = user_pk
|
|
else:
|
|
lookup_params["user"] = request.user
|
|
|
|
try:
|
|
obj.latest_vote = LatestVote.objects.get(**lookup_params).css_class()
|
|
except LatestVote.DoesNotExist:
|
|
obj.latest_vote = "no-vote"
|
|
|
|
yield obj
|
|
|
|
|
|
@login_required
|
|
def review_section(request, section_slug, assigned=False):
|
|
|
|
if not request.user.has_perm("reviews.can_review_%s" % section_slug):
|
|
return access_not_permitted(request)
|
|
|
|
section = get_object_or_404(ProposalSection, section__slug=section_slug)
|
|
queryset = ProposalBase.objects.filter(kind__section=section)
|
|
|
|
if assigned:
|
|
assignments = ReviewAssignment.objects.filter(user=request.user).values_list("proposal__id")
|
|
queryset = queryset.filter(id__in=assignments)
|
|
|
|
queryset = queryset.select_related("result").select_subclasses()
|
|
|
|
proposals = proposals_generator(request, queryset)
|
|
|
|
ctx = {
|
|
"proposals": proposals,
|
|
"section": section,
|
|
}
|
|
|
|
return render(request, "reviews/review_list.html", ctx)
|
|
|
|
|
|
@login_required
|
|
def review_list(request, section_slug, user_pk):
|
|
|
|
# if they're not a reviewer admin and they aren't the person whose
|
|
# review list is being asked for, don't let them in
|
|
if not request.user.has_perm("reviews.can_manage_%s" % section_slug):
|
|
if not request.user.pk == user_pk:
|
|
return access_not_permitted(request)
|
|
|
|
queryset = ProposalBase.objects.select_related("speaker__user", "result")
|
|
reviewed = LatestVote.objects.filter(user__pk=user_pk).values_list("proposal", flat=True)
|
|
queryset = queryset.filter(pk__in=reviewed)
|
|
proposals = queryset.order_by("submitted")
|
|
|
|
admin = request.user.has_perm("reviews.can_manage_%s" % section_slug)
|
|
|
|
proposals = proposals_generator(request, proposals, user_pk=user_pk, check_speaker=not admin)
|
|
|
|
ctx = {
|
|
"proposals": proposals,
|
|
}
|
|
return render(request, "reviews/review_list.html", ctx)
|
|
|
|
|
|
@login_required
|
|
def review_admin(request, section_slug):
|
|
|
|
if not request.user.has_perm("reviews.can_manage_%s" % section_slug):
|
|
return access_not_permitted(request)
|
|
|
|
def reviewers():
|
|
already_seen = set()
|
|
|
|
for team in Team.objects.filter(permissions__codename="can_review_%s" % section_slug):
|
|
for membership in team.memberships.filter(Q(state="member") | Q(state="manager")):
|
|
user = membership.user
|
|
if user.pk in already_seen:
|
|
continue
|
|
already_seen.add(user.pk)
|
|
|
|
user.comment_count = Review.objects.filter(user=user).count()
|
|
user.total_votes = LatestVote.objects.filter(user=user).count()
|
|
user.plus_one = LatestVote.objects.filter(
|
|
user = user,
|
|
vote = LatestVote.VOTES.PLUS_ONE
|
|
).count()
|
|
user.plus_zero = LatestVote.objects.filter(
|
|
user = user,
|
|
vote = LatestVote.VOTES.PLUS_ZERO
|
|
).count()
|
|
user.minus_zero = LatestVote.objects.filter(
|
|
user = user,
|
|
vote = LatestVote.VOTES.MINUS_ZERO
|
|
).count()
|
|
user.minus_one = LatestVote.objects.filter(
|
|
user = user,
|
|
vote = LatestVote.VOTES.MINUS_ONE
|
|
).count()
|
|
|
|
yield user
|
|
|
|
ctx = {
|
|
"section_slug": section_slug,
|
|
"reviewers": reviewers(),
|
|
}
|
|
return render(request, "reviews/review_admin.html", ctx)
|
|
|
|
|
|
@login_required
|
|
def review_detail(request, pk):
|
|
|
|
proposals = ProposalBase.objects.select_related("result").select_subclasses()
|
|
proposal = get_object_or_404(proposals, pk=pk)
|
|
|
|
if not request.user.has_perm("reviews.can_review_%s" % proposal.kind.section.slug):
|
|
return access_not_permitted(request)
|
|
|
|
speakers = [s.user for s in proposal.speakers()]
|
|
|
|
if not request.user.is_superuser and request.user in speakers:
|
|
return access_not_permitted(request)
|
|
|
|
admin = request.user.is_staff
|
|
|
|
try:
|
|
latest_vote = LatestVote.objects.get(proposal=proposal, user=request.user)
|
|
except LatestVote.DoesNotExist:
|
|
latest_vote = None
|
|
|
|
if request.method == "POST":
|
|
if request.user in speakers:
|
|
return access_not_permitted(request)
|
|
|
|
if "vote_submit" in request.POST:
|
|
review_form = ReviewForm(request.POST)
|
|
if review_form.is_valid():
|
|
|
|
review = review_form.save(commit=False)
|
|
review.user = request.user
|
|
review.proposal = proposal
|
|
review.save()
|
|
|
|
return redirect(request.path)
|
|
else:
|
|
message_form = SpeakerCommentForm()
|
|
elif "message_submit" in request.POST:
|
|
message_form = SpeakerCommentForm(request.POST)
|
|
if message_form.is_valid():
|
|
|
|
message = message_form.save(commit=False)
|
|
message.user = request.user
|
|
message.proposal = proposal
|
|
message.save()
|
|
|
|
for speaker in speakers:
|
|
if speaker and speaker.email:
|
|
ctx = {
|
|
"proposal": proposal,
|
|
"message": message,
|
|
"reviewer": False,
|
|
}
|
|
send_email(
|
|
[speaker.email], "proposal_new_message",
|
|
context = ctx
|
|
)
|
|
|
|
return redirect(request.path)
|
|
else:
|
|
initial = {}
|
|
if latest_vote:
|
|
initial["vote"] = latest_vote.vote
|
|
if request.user in speakers:
|
|
review_form = None
|
|
else:
|
|
review_form = ReviewForm(initial=initial)
|
|
elif "result_submit" in request.POST:
|
|
if admin:
|
|
result = request.POST["result_submit"]
|
|
|
|
if result == "accept":
|
|
proposal.result.status = "accepted"
|
|
proposal.result.save()
|
|
elif result == "reject":
|
|
proposal.result.status = "rejected"
|
|
proposal.result.save()
|
|
elif result == "undecide":
|
|
proposal.result.status = "undecided"
|
|
proposal.result.save()
|
|
elif result == "standby":
|
|
proposal.result.status = "standby"
|
|
proposal.result.save()
|
|
|
|
return redirect(request.path)
|
|
else:
|
|
initial = {}
|
|
if latest_vote:
|
|
initial["vote"] = latest_vote.vote
|
|
if request.user in speakers:
|
|
review_form = None
|
|
else:
|
|
review_form = ReviewForm(initial=initial)
|
|
message_form = SpeakerCommentForm()
|
|
|
|
proposal.comment_count = proposal.result.comment_count
|
|
proposal.total_votes = proposal.result.vote_count
|
|
proposal.plus_one = proposal.result.plus_one
|
|
proposal.plus_zero = proposal.result.plus_zero
|
|
proposal.minus_zero = proposal.result.minus_zero
|
|
proposal.minus_one = proposal.result.minus_one
|
|
|
|
reviews = Review.objects.filter(proposal=proposal).order_by("-submitted_at")
|
|
messages = proposal.messages.order_by("submitted_at")
|
|
|
|
return render(request, "reviews/review_detail.html", {
|
|
"proposal": proposal,
|
|
"latest_vote": latest_vote,
|
|
"reviews": reviews,
|
|
"review_messages": messages,
|
|
"review_form": review_form,
|
|
"message_form": message_form
|
|
})
|
|
|
|
|
|
@login_required
|
|
@require_POST
|
|
def review_delete(request, pk):
|
|
review = get_object_or_404(Review, pk=pk)
|
|
section_slug = review.section.slug
|
|
|
|
if not request.user.has_perm("reviews.can_manage_%s" % section_slug):
|
|
return access_not_permitted(request)
|
|
|
|
review = get_object_or_404(Review, pk=pk)
|
|
review.delete()
|
|
|
|
return redirect("review_detail", pk=review.proposal.pk)
|
|
|
|
|
|
@login_required
|
|
def review_status(request, section_slug=None, key=None):
|
|
|
|
if not request.user.has_perm("reviews.can_review_%s" % section_slug):
|
|
return access_not_permitted(request)
|
|
|
|
VOTE_THRESHOLD = settings.SYMPOSION_VOTE_THRESHOLD
|
|
|
|
ctx = {
|
|
"section_slug": section_slug,
|
|
"vote_threshold": VOTE_THRESHOLD,
|
|
}
|
|
|
|
queryset = ProposalBase.objects.select_related("speaker__user", "result").select_subclasses()
|
|
if section_slug:
|
|
queryset = queryset.filter(kind__section__slug=section_slug)
|
|
|
|
proposals = {
|
|
# proposals with at least VOTE_THRESHOLD reviews and at least one +1 and no -1s, sorted by the 'score'
|
|
"positive": queryset.filter(result__vote_count__gte=VOTE_THRESHOLD, result__plus_one__gt=0, result__minus_one=0).order_by("-result__score"),
|
|
# proposals with at least VOTE_THRESHOLD reviews and at least one -1 and no +1s, reverse sorted by the 'score'
|
|
"negative": queryset.filter(result__vote_count__gte=VOTE_THRESHOLD, result__minus_one__gt=0, result__plus_one=0).order_by("result__score"),
|
|
# proposals with at least VOTE_THRESHOLD reviews and neither a +1 or a -1, sorted by total votes (lowest first)
|
|
"indifferent": queryset.filter(result__vote_count__gte=VOTE_THRESHOLD, result__minus_one=0, result__plus_one=0).order_by("result__vote_count"),
|
|
# proposals with at least VOTE_THRESHOLD reviews and both a +1 and -1, sorted by total votes (highest first)
|
|
"controversial": queryset.filter(result__vote_count__gte=VOTE_THRESHOLD, result__plus_one__gt=0, result__minus_one__gt=0).order_by("-result__vote_count"),
|
|
# proposals with fewer than VOTE_THRESHOLD reviews
|
|
"too_few": queryset.filter(result__vote_count__lt=VOTE_THRESHOLD).order_by("result__vote_count"),
|
|
}
|
|
|
|
admin = request.user.has_perm("reviews.can_manage_%s" % section_slug)
|
|
|
|
for status in proposals:
|
|
if key and key != status:
|
|
continue
|
|
proposals[status] = list(proposals_generator(request, proposals[status], check_speaker=not admin))
|
|
|
|
if key:
|
|
ctx.update({
|
|
"key": key,
|
|
"proposals": proposals[key],
|
|
})
|
|
else:
|
|
ctx["proposals"] = proposals
|
|
|
|
return render(request, "reviews/review_stats.html", ctx)
|
|
|
|
|
|
@login_required
|
|
def review_assignments(request):
|
|
if not request.user.groups.filter(name="reviewers").exists():
|
|
return access_not_permitted(request)
|
|
assignments = ReviewAssignment.objects.filter(
|
|
user=request.user,
|
|
opted_out=False
|
|
)
|
|
return render(request, "reviews/review_assignment.html", {
|
|
"assignments": assignments,
|
|
})
|
|
|
|
|
|
@login_required
|
|
@require_POST
|
|
def review_assignment_opt_out(request, pk):
|
|
review_assignment = get_object_or_404(ReviewAssignment,
|
|
pk=pk,
|
|
user=request.user
|
|
)
|
|
if not review_assignment.opted_out:
|
|
review_assignment.opted_out = True
|
|
review_assignment.save()
|
|
ReviewAssignment.create_assignments(review_assignment.proposal, origin=ReviewAssignment.AUTO_ASSIGNED_LATER)
|
|
return redirect("review_assignments")
|
|
|
|
|
|
@login_required
|
|
def review_bulk_accept(request, section_slug):
|
|
if not request.user.has_perm("reviews.can_manage_%s" % section_slug):
|
|
return access_not_permitted(request)
|
|
if request.method == "POST":
|
|
form = BulkPresentationForm(request.POST)
|
|
if form.is_valid():
|
|
talk_ids = form.cleaned_data["talk_ids"].split(",")
|
|
talks = ProposalBase.objects.filter(id__in=talk_ids).select_related("result")
|
|
for talk in talks:
|
|
talk.result.status = "accepted"
|
|
talk.result.save()
|
|
return redirect("review_list")
|
|
else:
|
|
form = BulkPresentationForm()
|
|
|
|
return render(request, "reviews/review_bulk_accept.html", {
|
|
"form": form,
|
|
})
|
|
|
|
|
|
@login_required
|
|
def result_notification(request, section_slug, status):
|
|
if not request.user.has_perm("reviews.can_manage_%s" % section_slug):
|
|
return access_not_permitted(request)
|
|
|
|
proposals = ProposalBase.objects.filter(kind__section__slug=section_slug, result__status=status).select_related("speaker__user", "result").select_subclasses()
|
|
notification_templates = NotificationTemplate.objects.all()
|
|
|
|
ctx = {
|
|
"section_slug": section_slug,
|
|
"status": status,
|
|
"proposals": proposals,
|
|
"notification_templates": notification_templates,
|
|
}
|
|
return render(request, "reviews/result_notification.html", ctx)
|
|
|
|
|
|
@login_required
|
|
def result_notification_prepare(request, section_slug, status):
|
|
if request.method != "POST":
|
|
return HttpResponseNotAllowed(["POST"])
|
|
|
|
proposal_pks = []
|
|
try:
|
|
for pk in request.POST.getlist("_selected_action"):
|
|
proposal_pks.append(int(pk))
|
|
except ValueError:
|
|
return HttpResponseBadRequest()
|
|
proposals = ProposalBase.objects.filter(
|
|
kind__section__slug=section_slug,
|
|
result__status=status,
|
|
)
|
|
proposals = proposals.filter(pk__in=proposal_pks)
|
|
proposals = proposals.select_related("speaker__user", "result")
|
|
proposals = proposals.select_subclasses()
|
|
|
|
notification_template_pk = request.POST.get("notification_template", "")
|
|
if notification_template_pk:
|
|
notification_template = NotificationTemplate.objects.get(pk=notification_template_pk)
|
|
else:
|
|
notification_template = None
|
|
|
|
ctx = {
|
|
"section_slug": section_slug,
|
|
"status": status,
|
|
"notification_template": notification_template,
|
|
"proposals": proposals,
|
|
"proposal_pks": ",".join([str(pk) for pk in proposal_pks]),
|
|
}
|
|
return render(request, "reviews/result_notification_prepare.html", ctx)
|
|
|
|
|
|
def result_notification_send(request, section_slug, status):
|
|
if request.method != "POST":
|
|
return HttpResponseNotAllowed(["POST"])
|
|
|
|
if "proposal_pks" not in request.POST:
|
|
return HttpResponseBadRequest()
|
|
|
|
try:
|
|
proposal_pks = [int(pk) for pk in request.POST["proposal_pks"].split(",")]
|
|
except ValueError:
|
|
return HttpResponseBadRequest()
|
|
|
|
proposals = ProposalBase.objects.filter(
|
|
kind__section__slug=section_slug,
|
|
result__status=status,
|
|
)
|
|
proposals = proposals.filter(pk__in=proposal_pks)
|
|
proposals = proposals.select_related("speaker__user", "result")
|
|
proposals = proposals.select_subclasses()
|
|
|
|
notification_template_pk = request.POST.get("notification_template", "")
|
|
if notification_template_pk:
|
|
notification_template = NotificationTemplate.objects.get(pk=notification_template_pk)
|
|
else:
|
|
notification_template = None
|
|
|
|
# create ResultNotification objects and send
|
|
|
|
return redirect("result_notification", section_slug=section_slug, status=status)
|