Add a page for displaying a review request, add support for

withdrawing requests, add tests for these two pages
 - Legacy-Id: 11217
This commit is contained in:
Ole Laursen 2016-05-20 14:14:31 +00:00
parent 64a65340a2
commit 44e135345c
13 changed files with 247 additions and 30 deletions

View file

@ -707,6 +707,7 @@ EVENT_TYPES = [
# review
("requested_review", "Requested review"),
("withdrew_review_request", "Withdrew review"),
]
class DocEvent(models.Model):

View file

@ -1,33 +1,44 @@
# -*- coding: utf-8 -*-
import datetime
from pyquery import PyQuery
from django.core.urlresolvers import reverse as urlreverse
import debug # pyflakes:ignore
from ietf.review.models import ReviewRequest
from ietf.review.models import ReviewRequest, Reviewer
from ietf.person.models import Person
from ietf.group.models import Group, Role
from ietf.name.models import ReviewResultName
from ietf.name.models import ReviewResultName, ReviewRequestStateName
from ietf.utils.test_utils import TestCase
from ietf.utils.test_data import make_test_data
from ietf.utils.test_utils import login_testing_unauthorized
from ietf.utils.test_utils import login_testing_unauthorized, unicontent, reload_db_objects
def make_review_data():
review_team = Group.objects.create(state_id="active", acronym="reviewteam", name="Review Team", type_id="team")
review_team.reviewresultname_set.add(ReviewResultName.objects.filter(slug__in=["issues", "ready-issues", "ready", "not-ready"]))
def make_review_data(doc):
team = Group.objects.create(state_id="active", acronym="reviewteam", name="Review Team", type_id="team")
team.reviewresultname_set.add(ReviewResultName.objects.filter(slug__in=["issues", "ready-issues", "ready", "not-ready"]))
p = Person.objects.get(user__username="plain")
Role.objects.create(name_id="reviewer", person=p, email=p.email_set.first(), group=review_team)
role = Role.objects.create(name_id="reviewer", person=p, email=p.email_set.first(), group=team)
reviewer = Reviewer.objects.create(role=role, frequency=14, skip_next=0)
return review_team
review_req = ReviewRequest.objects.create(
doc=doc,
team=team,
type_id="early",
deadline=datetime.datetime.now() + datetime.timedelta(days=20),
state_id="ready",
reviewer=reviewer,
reviewed_rev="01",
)
return review_req
class ReviewTests(TestCase):
def test_request_review(self):
doc = make_test_data()
review_team = make_review_data()
review_req = make_review_data(doc)
review_team = review_req.team
url = urlreverse('ietf.doc.views_review.request_review', kwargs={ "name": doc.name })
login_testing_unauthorized(self, "secretary", url)
@ -47,17 +58,17 @@ class ReviewTests(TestCase):
})
self.assertEqual(r.status_code, 302)
req = ReviewRequest.objects.get(doc=doc)
req = ReviewRequest.objects.get(doc=doc, state="requested")
self.assertEqual(req.deadline.date(), deadline_date)
self.assertEqual(req.deadline.time(), datetime.time(23, 59, 59))
self.assertEqual(req.state_id, "requested")
self.assertEqual(req.team, review_team)
self.assertEqual(req.requested_rev, "01")
self.assertEqual(doc.latest_event().type, "requested_review")
def test_request_review_by_reviewer(self):
doc = make_test_data()
review_team = make_review_data()
review_req = make_review_data(doc)
review_team = review_req.team
url = urlreverse('ietf.doc.views_review.request_review', kwargs={ "name": doc.name })
login_testing_unauthorized(self, "plain", url)
@ -73,10 +84,40 @@ class ReviewTests(TestCase):
})
self.assertEqual(r.status_code, 302)
req = ReviewRequest.objects.get(doc=doc)
req = ReviewRequest.objects.get(doc=doc, state="requested")
self.assertEqual(req.state_id, "requested")
self.assertEqual(req.team, review_team)
def test_doc_page(self):
# FIXME: fill in
pass
def test_review_request(self):
doc = make_test_data()
review_req = make_review_data(doc)
url = urlreverse('ietf.doc.views_review.review_request', kwargs={ "name": doc.name, "request_id": review_req.pk })
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue(review_req.team.acronym.upper() in unicontent(r))
def test_withdraw_request(self):
doc = make_test_data()
review_req = make_review_data(doc)
review_req.state = ReviewRequestStateName.objects.get(slug="accepted")
review_req.save()
url = urlreverse('ietf.doc.views_review.withdraw_request', kwargs={ "name": doc.name, "request_id": review_req.pk })
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# withdraw
r = self.client.post(url, { "action": "withdraw" })
self.assertEqual(r.status_code, 302)
review_req = reload_db_objects(review_req)
self.assertEqual(review_req.state_id, "withdrawn")
self.assertEqual(doc.latest_event().type, "withdrew_review_request")

View file

@ -36,7 +36,6 @@ from django.views.generic import RedirectView
from ietf.doc import views_search, views_draft, views_ballot
from ietf.doc import views_status_change
from ietf.doc import views_doc
from ietf.doc import views_review
session_patterns = [
url(r'^add$', views_doc.add_sessionpresentation),
@ -74,8 +73,7 @@ urlpatterns = patterns('',
url(r'^(?P<name>[A-Za-z0-9._+-]+)/ballot/$', views_doc.document_ballot, name="doc_ballot"),
(r'^(?P<name>[A-Za-z0-9._+-]+)/(?:(?P<rev>[0-9-]+)/)?doc.json$', views_doc.document_json),
(r'^(?P<name>[A-Za-z0-9._+-]+)/ballotpopup/(?P<ballot_id>[0-9]+)/$', views_doc.ballot_popup),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/requestreview/$', views_review.request_review),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/review/(?P<request_id>[0-9]+)/$', views_review.review),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/reviewrequest/', include("ietf.doc.urls_review")),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/email-aliases/$', RedirectView.as_view(pattern_name='doc_email', permanent=False),name='doc_specific_email_aliases'),

9
ietf/doc/urls_review.py Normal file
View file

@ -0,0 +1,9 @@
from django.conf.urls import patterns, url
from ietf.doc import views_review
urlpatterns = patterns('',
url(r'^$', views_review.request_review),
url(r'^(?P<request_id>[0-9]+)/$', views_review.review_request),
url(r'^(?P<request_id>[0-9]+)/withdraw/$', views_review.withdraw_request),
)

View file

@ -2,7 +2,6 @@ import datetime
from django.http import HttpResponseForbidden
from django.shortcuts import render, get_object_or_404, redirect
from django.core.urlresolvers import reverse as urlreverse
from django import forms
from django.contrib.auth.decorators import login_required
@ -13,7 +12,6 @@ from ietf.review.models import ReviewRequest, ReviewRequestStateName
from ietf.review.utils import active_review_teams
from ietf.utils.fields import DatepickerDateField
class RequestReviewForm(forms.ModelForm):
deadline_date = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={ "autoclose": "1", "start-date": "+0d" })
deadline_time = forms.TimeField(widget=forms.TextInput(attrs={ 'placeholder': "HH:MM" }), help_text="If time is not specified, end of day is assumed", required=False)
@ -87,7 +85,8 @@ def request_review(request, name):
type="requested_review",
doc=doc,
by=request.user.person,
desc="{} review by {} requested".format(review_req.type.name, review_req.team.acronym.upper()),
desc="Requested {} review by {}".format(review_req.type.name, review_req.team.acronym.upper()),
time=review_req.time,
)
# FIXME: if I'm a reviewer, auto-assign to myself?
@ -101,8 +100,41 @@ def request_review(request, name):
'form': form,
})
def review(request, name, request_id):
def review_request(request, name, request_id):
doc = get_object_or_404(Document, name=name)
review_request = get_object_or_404(ReviewRequest, pk=request_id)
review_req = get_object_or_404(ReviewRequest, pk=request_id)
print doc, review_request
return render(request, 'doc/review/review_request.html', {
'doc': doc,
'review_req': review_req,
'can_withdraw_request': review_req.state_id in ["requested", "accepted"] and is_authorized_in_doc_stream(request.user, doc),
})
def withdraw_request(request, name, request_id):
doc = get_object_or_404(Document, name=name)
review_req = get_object_or_404(ReviewRequest, pk=request_id, state__in=["requested", "accepted"])
if not is_authorized_in_doc_stream(request.user, doc):
return HttpResponseForbidden("You do not have permission to perform this action")
if request.method == "POST" and request.POST.get("action") == "withdraw":
review_req.state = ReviewRequestStateName.objects.get(slug="withdrawn")
review_req.save()
DocEvent.objects.create(
type="withdrew_review_request",
doc=doc,
by=request.user.person,
desc="Withdrew request for {} review by {}".format(review_req.type.name, review_req.team.acronym.upper()),
)
if review_req.state_id != "requested":
# FIXME: handle this case - by emailing?
pass
return redirect(review_request, name=review_req.doc.name, request_id=review_req.pk)
return render(request, 'doc/review/withdraw_request.html', {
'doc': doc,
'review_req': review_req,
})

View file

@ -89,12 +89,12 @@ class LiaisonStatementTagName(NameModel):
"Action Required, Action Taken"
class ReviewRequestStateName(NameModel):
"""Requested, Accepted, Rejected, Withdrawn, Overtaken By Events,
No Response , Completed"""
No Response, Completed"""
class ReviewTypeName(NameModel):
"""Early Review, Last Call, Telechat"""
class ReviewResultName(NameModel):
"""Almost ready, Has issues, Has nits, Not Ready,
On the right track, Ready, Ready with issues,
Ready with nits, Serious Issues"""
teams = models.ManyToManyField("group.Group", help_text="Which teams this result can be set for. This also implicitly defines which teams are review teams - if there are no possible review results defined for a given team, it can't be a review team.", blank=True)
teams = models.ManyToManyField("group.Group", help_text="Which teams this result can be set for. This also implicitly defines which teams are review teams - if there are no possible review results defined for a given team, it can't be a review team.", blank=True)

View file

@ -21,7 +21,7 @@ class Migration(migrations.Migration):
('available', models.DateTimeField(help_text=b'When will this reviewer be available again', null=True, blank=True)),
('filter_re', models.CharField(max_length=255, blank=True)),
('skip_next', models.IntegerField(help_text=b'Skip the next N review assignments')),
('role', models.ForeignKey(to='group.Role')),
('role', models.OneToOneField(to='group.Role')),
],
options={
},

View file

@ -10,7 +10,7 @@ class Reviewer(models.Model):
of admin data associated with the reviewer in the particular team.
There will be one record for each combination of reviewer and team.
"""
role = models.ForeignKey(Role)
role = models.OneToOneField(Role)
frequency = models.IntegerField(help_text="Can review every N days")
available = models.DateTimeField(blank=True, null=True, help_text="When will this reviewer be available again")
filter_re = models.CharField(max_length=255, blank=True)

View file

@ -200,7 +200,7 @@
<td>
{% for r in review_requests %}
<div>
<a href="{% url "ietf.doc.views_review.review" doc.name r.pk %}">{{ r.team.acronym|upper }} {{ r.type.name }} Review ({{ r.state.name }})</a>
<a href="{% url "ietf.doc.views_review.review_request" doc.name r.pk %}">{{ r.team.acronym|upper }} {{ r.type.name }} Review ({{ r.state.name }})</a>
</div>
{% endfor %}

View file

@ -23,7 +23,7 @@
{% bootstrap_field form.requested_rev layout="horizontal" %}
{% buttons %}
<button type="submit" class="btn btn-primary" name="save_addresses" value="Save">Request review</button>
<button type="submit" class="btn btn-primary">Request review</button>
<a class="btn btn-default pull-right" href="{% url "doc_view" name=doc.canonical_name %}">Back</a>
{% endbuttons %}
</form>

View file

@ -0,0 +1,103 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2016, All Rights Reserved #}
{% load origin bootstrap3 static %}
{% block title %}Review request for {{ review_req.doc.name }}{% endblock %}
{% block content %}
{% origin %}
<h1>Review request<br><small>{{ review_req.doc.name }}</small></h1>
<table class="table table-condensed">
<tbody class="meta">
<tr>
<th>Request</th>
<th>Review of</th>
<td>
{% if review_req.requested_rev %}
<a href="{% url "doc_view" name=review_req.doc.name rev=review_req.requested_rev %}">{{ review_req.doc.name }}-<b>{{ review_req.requested_rev }}</b></a>
{% else %}
<a href="{% url "doc_view" name=review_req.doc.name %}">{{ review_req.doc.name }}</a>
{% endif %}
</td>
</tr>
<tr>
<th></th>
<th>Type</th>
<td>{{ review_req.type.name }} Review</td>
</tr>
<tr>
<th></th>
<th>Team</th>
<td>{{ review_req.team.acronym|upper }}</td>
</tr>
<tr>
<th></th>
<th>Deadline</th>
<td>
{% if review_req.deadline|date:"H:i" != "23:59" %}
{{ review_req.deadline|date:"Y-m-d H:i" }}
{% else %}
{{ review_req.deadline|date:"Y-m-d" }}
{% endif %}
</td>
</tr>
<tr>
<th></th>
<th>Requested</th>
<td>{{ review_req.time|date:"Y-m-d" }}</td>
</tr>
</tbody>
<tbody class="meta">
<tr>
<th>Review</th>
<th>State</th>
<td>{{ review_req.state.name }}</td>
</tr>
{% if review_req.reviewer %}
<tr>
<th></th>
<th>Reviewer</th>
<td>{{ review_req.reviewer.role.person }}</td>
</tr>
{% endif %}
{% if review_req.review %}
<tr>
<th></th>
<th>Review</th>
<td><a href="{{ review_req.review.get_absolute_url }}">{{ review_req.review.name }}</a></td>
</tr>
{% endif %}
{% if review_req.reviewed_rev %}
<tr>
<th></th>
<th>Reviewed revision</th>
<td><a href="">{{ review_req.reviewed_rev }}</a></td>
</tr>
{% endif %}
{% if review_req.result %}
<tr>
<th></th>
<th>Result of review</th>
<td>{{ review_req.result.name }</td>
</tr>
{% endif %}
</tbody>
</table>
<div class="buttonlist">
{% if can_withdraw_request %}
<a class="btn btn-default btn-xs" href="{% url "ietf.doc.views_review.withdraw_request" name=doc.name request_id=review_req.pk %}"><span class="fa fa-ban"></span> Withdraw request</a>
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1,22 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2016, All Rights Reserved #}
{% load origin bootstrap3 static %}
{% block title %}Withdraw review request for {{ review_req.doc.name }}{% endblock %}
{% block content %}
{% origin %}
<h1>Withdraw review request<br><small>{{ review_req.doc.name }}</small></h1>
<p>Do you want to withdraw the review request?</p>
<form method="post">
{% csrf_token %}
{% buttons %}
<a class="btn btn-default" href="{% url "ietf.doc.views_review.review_request" name=doc.canonical_name request_id=review_req.pk %}">Cancel</a>
<button type="submit" class="btn btn-primary" name="action" value="withdraw">Withdraw request</button>
{% endbuttons %}
</form>
{% endblock %}

View file

@ -255,7 +255,7 @@ def canonicalize_sitemap(s):
def login_testing_unauthorized(test_case, username, url, password=None):
r = test_case.client.get(url)
test_case.assertTrue(r.status_code in (302, 403))
test_case.assertIn(r.status_code, (302, 403))
if r.status_code == 302:
test_case.assertTrue("/accounts/login" in r['Location'])
if not password:
@ -272,6 +272,17 @@ def unicontent(r):
encoding = 'utf-8'
return r.content.decode(encoding)
def reload_db_objects(*objects):
"""Rerequest the given arguments from the database so they're refreshed, to be used like
foo, bar = reload_objects(foo, bar)"""
t = tuple(o.__class__.objects.get(pk=o.pk) for o in objects)
if len(objects) == 1:
return t[0]
else:
return t
class ReverseLazyTest(django.test.TestCase):
def test_redirect_with_lazy_reverse(self):
response = self.client.get('/ipr/update/')