add reviews app
This commit is contained in:
parent
67bca7473b
commit
3d68af9796
16 changed files with 977 additions and 0 deletions
0
symposion/reviews/__init__.py
Normal file
0
symposion/reviews/__init__.py
Normal file
13
symposion/reviews/context_processors.py
Normal file
13
symposion/reviews/context_processors.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
|
from symposion.proposals.models import ProposalSection
|
||||||
|
|
||||||
|
|
||||||
|
def reviews(request):
|
||||||
|
sections = []
|
||||||
|
for section in ProposalSection.objects.all():
|
||||||
|
if request.user.has_perm("reviews.can_review_%s" % section.section.slug):
|
||||||
|
sections.append(section)
|
||||||
|
return {
|
||||||
|
"review_sections": sections,
|
||||||
|
}
|
9
symposion/reviews/fixture_gen.py
Normal file
9
symposion/reviews/fixture_gen.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
|
||||||
|
from fixture_generator import fixture_generator
|
||||||
|
|
||||||
|
|
||||||
|
@fixture_generator(Group)
|
||||||
|
def initial_data():
|
||||||
|
Group.objects.create(name="reviewers")
|
||||||
|
Group.objects.create(name="reviewers-admins")
|
40
symposion/reviews/forms.py
Normal file
40
symposion/reviews/forms.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
from markitup.widgets import MarkItUpWidget
|
||||||
|
|
||||||
|
from symposion.reviews.models import Review, Comment, ProposalMessage, VOTES
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Review
|
||||||
|
fields = ["vote", "comment"]
|
||||||
|
widgets = { "comment": MarkItUpWidget() }
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(ReviewForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields["vote"] = forms.ChoiceField(
|
||||||
|
widget = forms.RadioSelect(),
|
||||||
|
choices = VOTES.CHOICES
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewCommentForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Comment
|
||||||
|
fields = ["text"]
|
||||||
|
widgets = { "text": MarkItUpWidget() }
|
||||||
|
|
||||||
|
|
||||||
|
class SpeakerCommentForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = ProposalMessage
|
||||||
|
fields = ["message"]
|
||||||
|
widgets = { "message": MarkItUpWidget() }
|
||||||
|
|
||||||
|
|
||||||
|
class BulkPresentationForm(forms.Form):
|
||||||
|
talk_ids = forms.CharField(
|
||||||
|
max_length=500,
|
||||||
|
help_text="Provide a comma seperated list of talk ids to accept."
|
||||||
|
)
|
0
symposion/reviews/management/__init__.py
Normal file
0
symposion/reviews/management/__init__.py
Normal file
0
symposion/reviews/management/commands/__init__.py
Normal file
0
symposion/reviews/management/commands/__init__.py
Normal file
11
symposion/reviews/management/commands/calculate_results.py
Normal file
11
symposion/reviews/management/commands/calculate_results.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
|
||||||
|
from symposion.reviews.models import ProposalResult
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
ProposalResult.full_calculate()
|
|
@ -0,0 +1,25 @@
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from django.contrib.auth.models import Permission
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
|
from symposion.proposals.models import ProposalSection
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
ct, created = ContentType.objects.get_or_create(
|
||||||
|
model="",
|
||||||
|
app_label="reviews",
|
||||||
|
defaults={"name": "reviews"}
|
||||||
|
)
|
||||||
|
|
||||||
|
for ps in ProposalSection.objects.all():
|
||||||
|
for action in ["review", "manage"]:
|
||||||
|
perm, created = Permission.objects.get_or_create(
|
||||||
|
codename="can_%s_%s" % (action, ps.section.slug),
|
||||||
|
content_type__pk=ct.id,
|
||||||
|
defaults={"name": "Can %s %s" % (action, ps), "content_type": ct}
|
||||||
|
)
|
||||||
|
print perm
|
15
symposion/reviews/management/commands/promoteproposals.py
Normal file
15
symposion/reviews/management/commands/promoteproposals.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db import connections
|
||||||
|
|
||||||
|
from symposion.reviews.models import ProposalResult, promote_proposal
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
accepted_proposals = ProposalResult.objects.filter(accepted=True)
|
||||||
|
accepted_proposals = accepted_proposals.order_by("proposal")
|
||||||
|
|
||||||
|
for result in accepted_proposals:
|
||||||
|
promote_proposal(result.proposal)
|
||||||
|
connections["default"].cursor().execute("SELECT setval('schedule_session_id_seq', (SELECT max(id) FROM schedule_session))")
|
309
symposion/reviews/models.py
Normal file
309
symposion/reviews/models.py
Normal file
|
@ -0,0 +1,309 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from datetime import datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
from markitup.fields import MarkupField
|
||||||
|
|
||||||
|
from symposion.proposals.models import ProposalBase
|
||||||
|
# from symposion.schedule.models import Presentation
|
||||||
|
|
||||||
|
|
||||||
|
class ProposalScoreExpression(object):
|
||||||
|
|
||||||
|
def as_sql(self, qn, connection=None):
|
||||||
|
sql = "((3 * plus_one + plus_zero) - (minus_zero + 3 * minus_one))"
|
||||||
|
return sql, []
|
||||||
|
|
||||||
|
def prepare_database_save(self, unused):
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class Votes(object):
|
||||||
|
PLUS_ONE = "+1"
|
||||||
|
PLUS_ZERO = "+0"
|
||||||
|
MINUS_ZERO = u"−0"
|
||||||
|
MINUS_ONE = u"−1"
|
||||||
|
|
||||||
|
CHOICES = [
|
||||||
|
(PLUS_ONE, u"+1 — Good proposal and I will argue for it to be accepted."),
|
||||||
|
(PLUS_ZERO, u"+0 — OK proposal, but I will not argue for it to be accepted."),
|
||||||
|
(MINUS_ZERO, u"−0 — Weak proposal, but I will not argue strongly against acceptance."),
|
||||||
|
(MINUS_ONE, u"−1 — Serious issues and I will argue to reject this proposal."),
|
||||||
|
]
|
||||||
|
VOTES = Votes()
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewAssignment(models.Model):
|
||||||
|
AUTO_ASSIGNED_INITIAL = 0
|
||||||
|
OPT_IN = 1
|
||||||
|
AUTO_ASSIGNED_LATER = 2
|
||||||
|
|
||||||
|
ORIGIN_CHOICES = [
|
||||||
|
(AUTO_ASSIGNED_INITIAL, "auto-assigned, initial"),
|
||||||
|
(OPT_IN, "opted-in"),
|
||||||
|
(AUTO_ASSIGNED_LATER, "auto-assigned, later"),
|
||||||
|
]
|
||||||
|
|
||||||
|
proposal = models.ForeignKey("proposals.ProposalBase")
|
||||||
|
user = models.ForeignKey(User)
|
||||||
|
|
||||||
|
origin = models.IntegerField(choices=ORIGIN_CHOICES)
|
||||||
|
|
||||||
|
assigned_at = models.DateTimeField(default=datetime.now)
|
||||||
|
opted_out = models.BooleanField()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_assignments(cls, proposal, origin=AUTO_ASSIGNED_INITIAL):
|
||||||
|
speakers = [proposal.speaker] + list(proposal.additional_speakers.all())
|
||||||
|
reviewers = User.objects.exclude(
|
||||||
|
pk__in=[
|
||||||
|
speaker.user_id
|
||||||
|
for speaker in speakers
|
||||||
|
if speaker.user_id is not None
|
||||||
|
]
|
||||||
|
).filter(
|
||||||
|
groups__name="reviewers",
|
||||||
|
).filter(
|
||||||
|
Q(reviewassignment__opted_out=False) | Q(reviewassignment=None)
|
||||||
|
).annotate(
|
||||||
|
num_assignments=models.Count("reviewassignment")
|
||||||
|
).order_by(
|
||||||
|
"num_assignments",
|
||||||
|
)
|
||||||
|
for reviewer in reviewers[:3]:
|
||||||
|
cls._default_manager.create(
|
||||||
|
proposal=proposal,
|
||||||
|
user=reviewer,
|
||||||
|
origin=origin,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProposalMessage(models.Model):
|
||||||
|
proposal = models.ForeignKey("proposals.ProposalBase", related_name="messages")
|
||||||
|
user = models.ForeignKey(User)
|
||||||
|
|
||||||
|
message = MarkupField()
|
||||||
|
submitted_at = models.DateTimeField(default=datetime.now, editable=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["submitted_at"]
|
||||||
|
|
||||||
|
|
||||||
|
class Review(models.Model):
|
||||||
|
VOTES = VOTES
|
||||||
|
|
||||||
|
proposal = models.ForeignKey("proposals.ProposalBase", related_name="reviews")
|
||||||
|
user = models.ForeignKey(User)
|
||||||
|
|
||||||
|
# No way to encode "-0" vs. "+0" into an IntegerField, and I don't feel
|
||||||
|
# like some complicated encoding system.
|
||||||
|
vote = models.CharField(max_length=2, blank=True, choices=VOTES.CHOICES)
|
||||||
|
comment = MarkupField()
|
||||||
|
submitted_at = models.DateTimeField(default=datetime.now, editable=False)
|
||||||
|
|
||||||
|
def save(self, **kwargs):
|
||||||
|
if self.vote:
|
||||||
|
vote, created = LatestVote.objects.get_or_create(
|
||||||
|
proposal = self.proposal,
|
||||||
|
user = self.user,
|
||||||
|
defaults = dict(
|
||||||
|
vote = self.vote,
|
||||||
|
submitted_at = self.submitted_at,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not created:
|
||||||
|
LatestVote.objects.filter(pk=vote.pk).update(vote=self.vote)
|
||||||
|
self.proposal.result.update_vote(self.vote, previous=vote.vote)
|
||||||
|
else:
|
||||||
|
self.proposal.result.update_vote(self.vote)
|
||||||
|
super(Review, self).save(**kwargs)
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
model = self.__class__
|
||||||
|
user_reviews = model._default_manager.filter(
|
||||||
|
proposal=self.proposal,
|
||||||
|
user=self.user,
|
||||||
|
)
|
||||||
|
# find previous vote before self
|
||||||
|
try:
|
||||||
|
previous = user_reviews.filter(submitted_at__lt=self.submitted_at).order_by("-submitted_at")[0]
|
||||||
|
except IndexError:
|
||||||
|
# did not find a previous which means this must be the only one.
|
||||||
|
# treat it as a last, but delete the latest vote.
|
||||||
|
self.proposal.result.update_vote(self.vote, removal=True)
|
||||||
|
lv = LatestVote.objects.filter(proposal=self.proposal, user=self.user)
|
||||||
|
lv.delete()
|
||||||
|
else:
|
||||||
|
# handle that we've found a previous vote
|
||||||
|
# check if self is the last vote
|
||||||
|
if self == user_reviews.order_by("-submitted_at")[0]:
|
||||||
|
# self is the latest which means we need to treat as last.
|
||||||
|
# revert the latest vote to previous vote.
|
||||||
|
self.proposal.result.update_vote(self.vote, previous=previous.vote, removal=True)
|
||||||
|
lv = LatestVote.objects.filter(proposal=self.proposal, user=self.user)
|
||||||
|
lv.update(
|
||||||
|
vote=previous.vote,
|
||||||
|
submitted_at=previous.submitted_at,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# self is not the latest so we just need to decrement the
|
||||||
|
# comment count
|
||||||
|
self.proposal.result.comment_count = models.F("comment_count") - 1
|
||||||
|
self.proposal.result.save()
|
||||||
|
# in all cases we need to delete the review; let's do it!
|
||||||
|
super(Review, self).delete()
|
||||||
|
|
||||||
|
def css_class(self):
|
||||||
|
return {
|
||||||
|
self.VOTES.PLUS_ONE: "plus-one",
|
||||||
|
self.VOTES.PLUS_ZERO: "plus-zero",
|
||||||
|
self.VOTES.MINUS_ZERO: "minus-zero",
|
||||||
|
self.VOTES.MINUS_ONE: "minus-one",
|
||||||
|
}[self.vote]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def section(self):
|
||||||
|
return self.proposal.kind.section.slug
|
||||||
|
|
||||||
|
|
||||||
|
class LatestVote(models.Model):
|
||||||
|
VOTES = VOTES
|
||||||
|
|
||||||
|
proposal = models.ForeignKey("proposals.ProposalBase", related_name="votes")
|
||||||
|
user = models.ForeignKey(User)
|
||||||
|
|
||||||
|
# No way to encode "-0" vs. "+0" into an IntegerField, and I don't feel
|
||||||
|
# like some complicated encoding system.
|
||||||
|
vote = models.CharField(max_length=2, choices=VOTES.CHOICES)
|
||||||
|
submitted_at = models.DateTimeField(default=datetime.now, editable=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = [("proposal", "user")]
|
||||||
|
|
||||||
|
def css_class(self):
|
||||||
|
return {
|
||||||
|
self.VOTES.PLUS_ONE: "plus-one",
|
||||||
|
self.VOTES.PLUS_ZERO: "plus-zero",
|
||||||
|
self.VOTES.MINUS_ZERO: "minus-zero",
|
||||||
|
self.VOTES.MINUS_ONE: "minus-one",
|
||||||
|
}[self.vote]
|
||||||
|
|
||||||
|
|
||||||
|
class ProposalResult(models.Model):
|
||||||
|
proposal = models.OneToOneField("proposals.ProposalBase", related_name="result")
|
||||||
|
score = models.DecimalField(max_digits=5, decimal_places=2, default=Decimal("0.00"))
|
||||||
|
comment_count = models.PositiveIntegerField(default=1)
|
||||||
|
vote_count = models.PositiveIntegerField(default=1)
|
||||||
|
plus_one = models.PositiveIntegerField(default=0)
|
||||||
|
plus_zero = models.PositiveIntegerField(default=0)
|
||||||
|
minus_zero = models.PositiveIntegerField(default=0)
|
||||||
|
minus_one = models.PositiveIntegerField(default=0)
|
||||||
|
accepted = models.NullBooleanField(choices=[
|
||||||
|
(True, "accepted"),
|
||||||
|
(False, "rejected"),
|
||||||
|
(None, "undecided"),
|
||||||
|
], default=None)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def full_calculate(cls):
|
||||||
|
for proposal in ProposalBase.objects.all():
|
||||||
|
result, created = cls._default_manager.get_or_create(proposal=proposal)
|
||||||
|
result.comment_count = Review.objects.filter(proposal=proposal).count()
|
||||||
|
result.vote_count = LatestVote.objects.filter(proposal=proposal).count()
|
||||||
|
result.plus_one = LatestVote.objects.filter(
|
||||||
|
proposal = proposal,
|
||||||
|
vote = VOTES.PLUS_ONE
|
||||||
|
).count()
|
||||||
|
result.plus_zero = LatestVote.objects.filter(
|
||||||
|
proposal = proposal,
|
||||||
|
vote = VOTES.PLUS_ZERO
|
||||||
|
).count()
|
||||||
|
result.minus_zero = LatestVote.objects.filter(
|
||||||
|
proposal = proposal,
|
||||||
|
vote = VOTES.MINUS_ZERO
|
||||||
|
).count()
|
||||||
|
result.minus_one = LatestVote.objects.filter(
|
||||||
|
proposal = proposal,
|
||||||
|
vote = VOTES.MINUS_ONE
|
||||||
|
).count()
|
||||||
|
result.save()
|
||||||
|
cls._default_manager.filter(pk=result.pk).update(score=ProposalScoreExpression())
|
||||||
|
|
||||||
|
def update_vote(self, vote, previous=None, removal=False):
|
||||||
|
mapping = {
|
||||||
|
VOTES.PLUS_ONE: "plus_one",
|
||||||
|
VOTES.PLUS_ZERO: "plus_zero",
|
||||||
|
VOTES.MINUS_ZERO: "minus_zero",
|
||||||
|
VOTES.MINUS_ONE: "minus_one",
|
||||||
|
}
|
||||||
|
if previous:
|
||||||
|
if previous == vote:
|
||||||
|
return
|
||||||
|
if removal:
|
||||||
|
setattr(self, mapping[previous], models.F(mapping[previous]) + 1)
|
||||||
|
else:
|
||||||
|
setattr(self, mapping[previous], models.F(mapping[previous]) - 1)
|
||||||
|
else:
|
||||||
|
if removal:
|
||||||
|
self.vote_count = models.F("vote_count") - 1
|
||||||
|
else:
|
||||||
|
self.vote_count = models.F("vote_count") + 1
|
||||||
|
if removal:
|
||||||
|
setattr(self, mapping[vote], models.F(mapping[vote]) - 1)
|
||||||
|
self.comment_count = models.F("comment_count") - 1
|
||||||
|
else:
|
||||||
|
setattr(self, mapping[vote], models.F(mapping[vote]) + 1)
|
||||||
|
self.comment_count = models.F("comment_count") + 1
|
||||||
|
self.save()
|
||||||
|
model = self.__class__
|
||||||
|
model._default_manager.filter(pk=self.pk).update(score=ProposalScoreExpression())
|
||||||
|
|
||||||
|
|
||||||
|
class Comment(models.Model):
|
||||||
|
proposal = models.ForeignKey("proposals.ProposalBase", related_name="comments")
|
||||||
|
commenter = models.ForeignKey(User)
|
||||||
|
text = MarkupField()
|
||||||
|
|
||||||
|
# Or perhaps more accurately, can the user see this comment.
|
||||||
|
public = models.BooleanField(choices=[
|
||||||
|
(True, "public"),
|
||||||
|
(False, "private"),
|
||||||
|
])
|
||||||
|
commented_at = models.DateTimeField(default=datetime.now)
|
||||||
|
|
||||||
|
|
||||||
|
def promote_proposal(proposal):
|
||||||
|
raise NotImplementedError()
|
||||||
|
# presentation, created = Presentation.objects.get_or_create(
|
||||||
|
# pk=proposal.pk,
|
||||||
|
# defaults=dict(
|
||||||
|
# title=proposal.title,
|
||||||
|
# description=proposal.description,
|
||||||
|
# kind=proposal.kind,
|
||||||
|
# category=proposal.category,
|
||||||
|
# duration=proposal.duration,
|
||||||
|
# abstract=proposal.abstract,
|
||||||
|
# audience_level=proposal.audience_level,
|
||||||
|
# submitted=proposal.submitted,
|
||||||
|
# speaker=proposal.speaker,
|
||||||
|
# )
|
||||||
|
# )
|
||||||
|
# if created:
|
||||||
|
# for speaker in proposal.additional_speakers.all():
|
||||||
|
# presentation.additional_speakers.add(speaker)
|
||||||
|
# presentation.save()
|
||||||
|
# return presentation
|
||||||
|
|
||||||
|
|
||||||
|
def accepted_proposal(sender, instance=None, **kwargs):
|
||||||
|
if instance is None:
|
||||||
|
return
|
||||||
|
if instance.accepted == True:
|
||||||
|
promote_proposal(instance.proposal)
|
||||||
|
post_save.connect(accepted_proposal, sender=ProposalResult)
|
0
symposion/reviews/templatetags/__init__.py
Normal file
0
symposion/reviews/templatetags/__init__.py
Normal file
20
symposion/reviews/templatetags/review_tags.py
Normal file
20
symposion/reviews/templatetags/review_tags.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
from django import template
|
||||||
|
|
||||||
|
from symposion.reviews.models import Review, ReviewAssignment
|
||||||
|
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.assignment_tag(takes_context=True)
|
||||||
|
def user_reviews(context):
|
||||||
|
request = context["request"]
|
||||||
|
reviews = Review.objects.filter(user=request.user)
|
||||||
|
return reviews
|
||||||
|
|
||||||
|
|
||||||
|
@register.assignment_tag(takes_context=True)
|
||||||
|
def review_assignments(context):
|
||||||
|
request = context["request"]
|
||||||
|
assignments = ReviewAssignment.objects.filter(user=request.user)
|
||||||
|
return assignments
|
143
symposion/reviews/tests.py
Normal file
143
symposion/reviews/tests.py
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User, Group
|
||||||
|
|
||||||
|
from symposion.proposals.models import Proposal
|
||||||
|
from symposion.reviews.models import Review, ReviewAssignment
|
||||||
|
|
||||||
|
|
||||||
|
class login(object):
|
||||||
|
def __init__(self, testcase, user, password):
|
||||||
|
self.testcase = testcase
|
||||||
|
success = testcase.client.login(username=user, password=password)
|
||||||
|
self.testcase.assertTrue(
|
||||||
|
success,
|
||||||
|
"login with username=%r, password=%r failed" % (user, password)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
self.testcase.client.logout()
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewTests(TestCase):
|
||||||
|
fixtures = ["proposals"]
|
||||||
|
|
||||||
|
def get(self, url_name, *args, **kwargs):
|
||||||
|
return self.client.get(reverse(url_name, args=args, kwargs=kwargs))
|
||||||
|
|
||||||
|
def post(self, url_name, *args, **kwargs):
|
||||||
|
data = kwargs.pop("data")
|
||||||
|
return self.client.post(reverse(url_name, args=args, kwargs=kwargs), data)
|
||||||
|
|
||||||
|
def login(self, user, password):
|
||||||
|
return login(self, user, password)
|
||||||
|
|
||||||
|
def test_detail_perms(self):
|
||||||
|
guidos_proposal = Proposal.objects.all()[0]
|
||||||
|
response = self.get("review_detail", pk=guidos_proposal.pk)
|
||||||
|
|
||||||
|
# Not logged in
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
with self.login("guido", "pythonisawesome"):
|
||||||
|
response = self.get("review_detail", pk=guidos_proposal.pk)
|
||||||
|
# Guido can see his own proposal.
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
with self.login("matz", "pythonsucks"):
|
||||||
|
response = self.get("review_detail", pk=guidos_proposal.pk)
|
||||||
|
# Matz can't see guido's proposal
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
larry = User.objects.get(username="larryw")
|
||||||
|
# Larry is a trustworthy guy, he's a reviewer.
|
||||||
|
larry.groups.add(Group.objects.get(name="reviewers"))
|
||||||
|
with self.login("larryw", "linenoisehere"):
|
||||||
|
response = self.get("review_detail", pk=guidos_proposal.pk)
|
||||||
|
# Reviewers can see a review detail page.
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_reviewing(self):
|
||||||
|
guidos_proposal = Proposal.objects.all()[0]
|
||||||
|
|
||||||
|
with self.login("guido", "pythonisawesome"):
|
||||||
|
response = self.post("review_review", pk=guidos_proposal.pk, data={
|
||||||
|
"vote": "+1",
|
||||||
|
})
|
||||||
|
# It redirects, but...
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
# ... no vote recorded
|
||||||
|
self.assertEqual(guidos_proposal.reviews.count(), 0)
|
||||||
|
|
||||||
|
larry = User.objects.get(username="larryw")
|
||||||
|
# Larry is a trustworthy guy, he's a reviewer.
|
||||||
|
larry.groups.add(Group.objects.get(name="reviewers"))
|
||||||
|
with self.login("larryw", "linenoisehere"):
|
||||||
|
response = self.post("review_review", pk=guidos_proposal.pk, data={
|
||||||
|
"vote": "+0",
|
||||||
|
"text": "Looks like a decent proposal, and Guido is a smart guy",
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(guidos_proposal.reviews.count(), 1)
|
||||||
|
self.assertEqual(ReviewAssignment.objects.count(), 1)
|
||||||
|
assignment = ReviewAssignment.objects.get()
|
||||||
|
self.assertEqual(assignment.proposal, guidos_proposal)
|
||||||
|
self.assertEqual(assignment.origin, ReviewAssignment.OPT_IN)
|
||||||
|
self.assertEqual(guidos_proposal.comments.count(), 1)
|
||||||
|
comment = guidos_proposal.comments.get()
|
||||||
|
self.assertFalse(comment.public)
|
||||||
|
|
||||||
|
response = self.post("review_review", pk=guidos_proposal.pk, data={
|
||||||
|
"vote": "+1",
|
||||||
|
"text": "Actually Perl is dead, we really need a talk on the future",
|
||||||
|
})
|
||||||
|
self.assertEqual(guidos_proposal.reviews.count(), 2)
|
||||||
|
self.assertEqual(ReviewAssignment.objects.count(), 1)
|
||||||
|
assignment = ReviewAssignment.objects.get()
|
||||||
|
self.assertEqual(assignment.review, Review.objects.order_by("-id")[0])
|
||||||
|
self.assertEqual(guidos_proposal.comments.count(), 2)
|
||||||
|
|
||||||
|
# Larry's a big fan...
|
||||||
|
response = self.post("review_review", pk=guidos_proposal.pk, data={
|
||||||
|
"vote": "+20",
|
||||||
|
})
|
||||||
|
self.assertEqual(guidos_proposal.reviews.count(), 2)
|
||||||
|
|
||||||
|
def test_speaker_commenting(self):
|
||||||
|
guidos_proposal = Proposal.objects.all()[0]
|
||||||
|
|
||||||
|
with self.login("guido", "pythonisawesome"):
|
||||||
|
response = self.get("review_comment", pk=guidos_proposal.pk)
|
||||||
|
# Guido can comment on his proposal.
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.post("review_comment", pk=guidos_proposal.pk, data={
|
||||||
|
"text": "FYI I can do this as a 30-minute or 45-minute talk.",
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(guidos_proposal.comments.count(), 1)
|
||||||
|
comment = guidos_proposal.comments.get()
|
||||||
|
self.assertTrue(comment.public)
|
||||||
|
|
||||||
|
larry = User.objects.get(username="larryw")
|
||||||
|
# Larry is a trustworthy guy, he's a reviewer.
|
||||||
|
larry.groups.add(Group.objects.get(name="reviewers"))
|
||||||
|
with self.login("larryw", "linenoisehere"):
|
||||||
|
response = self.get("review_comment", pk=guidos_proposal.pk)
|
||||||
|
# Larry can comment, since he's a reviewer
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.post("review_comment", pk=guidos_proposal.pk, data={
|
||||||
|
"text": "Thanks for the heads-up Guido."
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(guidos_proposal.comments.count(), 2)
|
||||||
|
|
||||||
|
with self.login("matz", "pythonsucks"):
|
||||||
|
response = self.get("review_comment", pk=guidos_proposal.pk)
|
||||||
|
# Matz can't comment.
|
||||||
|
self.assertEqual(response.status_code, 302)
|
18
symposion/reviews/urls.py
Normal file
18
symposion/reviews/urls.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
from django.conf.urls.defaults import patterns, url
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = patterns("symposion.reviews.views",
|
||||||
|
url(r"^section/(?P<section_slug>[\w\-]+)/$", "review_section", name="review_section"),
|
||||||
|
url(r"^section/(?P<section_slug>[\w\-]+)/assignments/$", "review_section", {"assigned": True}, name="review_section_assignments"),
|
||||||
|
url(r"^section/(?P<section_slug>[\w\-]+)/status/$", "review_status", name="review_status"),
|
||||||
|
url(r"^section/(?P<section_slug>[\w\-]+)/status/(?P<key>[\w]+)/$", "review_status", name="review_status"),
|
||||||
|
url(r"^section/(?P<section_slug>[\w\-]+)/list/(?P<user_pk>\d+)/$", "review_list", name="review_list_user"),
|
||||||
|
url(r"^section/(?P<section_slug>[\w\-]+)/admin/$", "review_admin", name="review_admin"),
|
||||||
|
url(r"^section/(?P<section_slug>[\w\-]+)/admin/accept/$", "review_bulk_accept", name="review_bulk_accept"),
|
||||||
|
|
||||||
|
url(r"^review/(?P<pk>\d+)/$", "review_detail", name="review_detail"),
|
||||||
|
|
||||||
|
url(r"^(?P<pk>\d+)/delete/$", "review_delete", name="review_delete"),
|
||||||
|
url(r"^assignments/$", "review_assignments", name="review_assignments"),
|
||||||
|
url(r"^assignment/(?P<pk>\d+)/opt-out/$", "review_assignment_opt_out", name="review_assignment_opt_out"),
|
||||||
|
)
|
19
symposion/reviews/utils.py
Normal file
19
symposion/reviews/utils.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
def has_permission(user, proposal, speaker=False, reviewer=False):
|
||||||
|
"""
|
||||||
|
Returns whether or not ther user has permission to review this proposal,
|
||||||
|
with the specified requirements.
|
||||||
|
|
||||||
|
If ``speaker`` is ``True`` then the user can be one of the speakers for the
|
||||||
|
proposal. If ``reviewer`` is ``True`` the speaker can be a part of the
|
||||||
|
reviewer group.
|
||||||
|
"""
|
||||||
|
if user.is_superuser:
|
||||||
|
return True
|
||||||
|
if speaker:
|
||||||
|
if (user == proposal.speaker.user or
|
||||||
|
proposal.additional_speakers.filter(user=user).exists()):
|
||||||
|
return True
|
||||||
|
if reviewer:
|
||||||
|
if user.groups.filter(name="reviewers").exists():
|
||||||
|
return True
|
||||||
|
return False
|
355
symposion/reviews/views.py
Normal file
355
symposion/reviews/views.py
Normal file
|
@ -0,0 +1,355 @@
|
||||||
|
from django.db.models import Q
|
||||||
|
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.proposals.models import ProposalBase, ProposalSection
|
||||||
|
from symposion.reviews.forms import ReviewForm, SpeakerCommentForm
|
||||||
|
from symposion.reviews.forms import BulkPresentationForm
|
||||||
|
from symposion.reviews.models import ReviewAssignment, Review, LatestVote, ProposalResult
|
||||||
|
from symposion.teams.models import Team
|
||||||
|
from symposion.utils.mail import send_email
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
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.accepted = True
|
||||||
|
proposal.result.save()
|
||||||
|
elif result == "reject":
|
||||||
|
proposal.result.accepted = False
|
||||||
|
proposal.result.save()
|
||||||
|
elif result == "undecide":
|
||||||
|
proposal.result.accepted = None
|
||||||
|
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):
|
||||||
|
|
||||||
|
ctx = {
|
||||||
|
"section_slug": section_slug
|
||||||
|
}
|
||||||
|
|
||||||
|
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 3 reviews and at least one +1 and no -1s, sorted by the 'score'
|
||||||
|
"positive": queryset.filter(result__vote_count__gte=3, result__plus_one__gt=0, result__minus_one=0).order_by("-result__score"),
|
||||||
|
# proposals with at least 3 reviews and at least one -1 and no +1s, reverse sorted by the 'score'
|
||||||
|
"negative": queryset.filter(result__vote_count__gte=3, result__minus_one__gt=0, result__plus_one=0).order_by("result__score"),
|
||||||
|
# proposals with at least 3 reviews and neither a +1 or a -1, sorted by total votes (lowest first)
|
||||||
|
"indifferent": queryset.filter(result__vote_count__gte=3, result__minus_one=0, result__plus_one=0).order_by("result__vote_count"),
|
||||||
|
# proposals with at least 3 reviews and both a +1 and -1, sorted by total votes (highest first)
|
||||||
|
"controversial": queryset.filter(result__vote_count__gte=3, result__plus_one__gt=0, result__minus_one__gt=0).order_by("-result__vote_count"),
|
||||||
|
# proposals with fewer than 3 reviews
|
||||||
|
"too_few": queryset.filter(result__vote_count__lt=3).order_by("result__vote_count"),
|
||||||
|
}
|
||||||
|
|
||||||
|
admin = request.user.has_perm("reviews.can_manage_%s" % section_slug)
|
||||||
|
|
||||||
|
if key:
|
||||||
|
ctx.update({
|
||||||
|
"key": key,
|
||||||
|
"proposals": proposals_generator(request, proposals[key], check_speaker=not admin),
|
||||||
|
"proposal_count": proposals[key].count(),
|
||||||
|
})
|
||||||
|
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.accepted = True
|
||||||
|
talk.result.save()
|
||||||
|
return redirect("review_list")
|
||||||
|
else:
|
||||||
|
form = BulkPresentationForm()
|
||||||
|
|
||||||
|
return render(request, "reviews/review_bulk_accept.html", {
|
||||||
|
"form": form,
|
||||||
|
})
|
Loading…
Add table
Reference in a new issue