Fix #2050 - Allow adding review wishes from document and search pages.

On the main page of a document and in document search results, a new
button allows review team members to add a review wish for that document.

For reviewers that are only on one team, this essentially works
identical to tracking a document. Reviewers that are on multiple teams
are lead through an intermediate step to select a review team, and then
returned to their search or document page.

Commit ready for merge.
 - Legacy-Id: 16939
This commit is contained in:
Sasha Romijn 2019-10-29 16:27:56 +00:00
parent f075eced5c
commit 6e55f26dbd
12 changed files with 234 additions and 37 deletions

View file

@ -58,20 +58,6 @@ def can_manage_community_list(user, clist):
return False
def augment_docs_with_tracking_info(docs, user):
"""Add attribute to each document with whether the document is tracked
by the user or not."""
tracked = set()
if user and user.is_authenticated:
clist = CommunityList.objects.filter(user=user).first()
if clist:
tracked.update(docs_tracked_by_community_list(clist).filter(pk__in=[ d.pk for d in docs ]).values_list("pk", flat=True))
for d in docs:
d.tracked_in_personal_community_list = d.pk in tracked
def reset_name_contains_index_for_rule(rule):
if not rule.rule_type == "name_contains":
return

View file

@ -22,7 +22,8 @@ from pyquery import PyQuery
import debug # pyflakes:ignore
import ietf.review.mailarch
from ietf.doc.factories import NewRevisionDocEventFactory, WgDraftFactory, WgRfcFactory, ReviewFactory
from ietf.doc.factories import NewRevisionDocEventFactory, WgDraftFactory, WgRfcFactory, \
ReviewFactory, DocumentFactory
from ietf.doc.models import DocumentAuthor, RelatedDocument, DocEvent, ReviewRequestDocEvent, ReviewAssignmentDocEvent
from ietf.group.factories import RoleFactory, ReviewTeamFactory
from ietf.group.models import Group
@ -1164,3 +1165,51 @@ class ReviewTests(TestCase):
assignment = reload_db_objects(assignment)
self.assertEqual(assignment.state_id, 'withdrawn')
def test_review_wish_add(self):
doc = DocumentFactory()
team = ReviewTeamFactory()
reviewer = RoleFactory(group=team, name_id='reviewer').person
url = urlreverse('ietf.doc.views_review.review_wish_add', kwargs={'name': doc.name})
login_testing_unauthorized(self, reviewer.user.username, url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# As this reviewer is only on a single team, posting without data should work
r = self.client.post(url + '?next=/redirect-url')
self.assertRedirects(r, '/redirect-url', fetch_redirect_response=False)
self.assertTrue(ReviewWish.objects.get(person=reviewer, doc=doc, team=team))
# Try again with a reviewer on multiple teams, requiring team selection.
# This also uses an invalid redirect URL that should be ignored.
ReviewWish.objects.all().delete()
team2 = ReviewTeamFactory()
RoleFactory(group=team2, person=reviewer, name_id='reviewer')
r = self.client.post(url + '?next=http://example.com/')
self.assertEqual(r.status_code, 200) # Missing team parameter
r = self.client.post(url + '?next=http://example.com/', data={'team': team2.pk})
self.assertRedirects(r, doc.get_absolute_url(), fetch_redirect_response=False)
self.assertTrue(ReviewWish.objects.get(person=reviewer, doc=doc, team=team2))
def test_review_wishes_remove(self):
doc = DocumentFactory()
team = ReviewTeamFactory()
reviewer = RoleFactory(group=team, name_id='reviewer').person
ReviewWish.objects.create(person=reviewer, doc=doc, team=team)
url = urlreverse('ietf.doc.views_review.review_wishes_remove', kwargs={'name': doc.name})
login_testing_unauthorized(self, reviewer.user.username, url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
r = self.client.post(url + '?next=/redirect-url')
self.assertRedirects(r, '/redirect-url', fetch_redirect_response=False)
self.assertFalse(ReviewWish.objects.all())
# Try again with an invalid redirect URL that should be ignored.
ReviewWish.objects.create(person=reviewer, doc=doc, team=team)
r = self.client.post(url + '?next=http://example.com')
self.assertRedirects(r, doc.get_absolute_url(), fetch_redirect_response=False)
self.assertFalse(ReviewWish.objects.all())

View file

@ -20,4 +20,6 @@ urlpatterns = [
url(r'^team/%(acronym)s/searchmailarchive/$' % settings.URL_REGEXPS, views_review.search_mail_archive),
url(r'^(?P<request_id>[0-9]+)/editcomment/$', views_review.edit_comment),
url(r'^(?P<request_id>[0-9]+)/editdeadline/$', views_review.edit_deadline),
url(r'^addreviewwish/$' % settings.URL_REGEXPS, views_review.review_wish_add),
url(r'^removereviewwishes/$' % settings.URL_REGEXPS, views_review.review_wishes_remove),
]

View file

@ -23,14 +23,18 @@ from django.utils.html import escape
from django.urls import reverse as urlreverse
import debug # pyflakes:ignore
from ietf.community.models import CommunityList
from ietf.community.utils import docs_tracked_by_community_list
from ietf.doc.models import Document, DocHistory, State, DocumentAuthor, DocHistoryAuthor
from ietf.doc.models import DocAlias, RelatedDocument, RelatedDocHistory, BallotType, DocReminder
from ietf.doc.models import DocEvent, ConsensusDocEvent, BallotDocEvent, NewRevisionDocEvent, StateDocEvent
from ietf.doc.models import TelechatDocEvent
from ietf.name.models import DocReminderTypeName, DocRelationshipName
from ietf.group.models import Role
from ietf.group.models import Role, Group
from ietf.ietfauth.utils import has_role
from ietf.person.models import Person
from ietf.review.models import ReviewWish
from ietf.utils import draft, text
from ietf.utils.mail import send_mail
from ietf.mailtrigger.utils import gather_address_lists
@ -903,3 +907,31 @@ def build_doc_meta_block(doc, path):
#
return block
def augment_docs_and_user_with_user_info(docs, user):
"""Add attribute to each document with whether the document is tracked
or has a review wish by the user or not, and the review teams the user is on."""
tracked = set()
review_wished = set()
if user and user.is_authenticated:
user.review_teams = Group.objects.filter(
reviewteamsettings__isnull=False, role__person__user=user, role__name='reviewer')
doc_pks = [d.pk for d in docs]
clist = CommunityList.objects.filter(user=user).first()
if clist:
tracked.update(
docs_tracked_by_community_list(clist).filter(pk__in=doc_pks).values_list("pk", flat=True))
try:
wishes = ReviewWish.objects.filter(person=Person.objects.get(user=user))
wishes = wishes.filter(doc__pk__in=doc_pks).values_list("doc__pk", flat=True)
review_wished.update(wishes)
except Person.DoesNotExist:
pass
for d in docs:
d.tracked_in_personal_community_list = d.pk in tracked
d.has_review_wish = d.pk in review_wished

View file

@ -7,9 +7,9 @@ from __future__ import absolute_import, print_function, unicode_literals
import datetime
import debug # pyflakes:ignore
from ietf.community.utils import augment_docs_with_tracking_info
from ietf.doc.models import Document, DocAlias, RelatedDocument, DocEvent, TelechatDocEvent, BallotDocEvent
from ietf.doc.expire import expirable_draft
from ietf.doc.utils import augment_docs_and_user_with_user_info
from ietf.meeting.models import SessionPresentation, Meeting, Session
def wrap_value(v):
@ -162,7 +162,7 @@ def prepare_document_table(request, docs, query=None, max_results=200):
docs = list(docs)
fill_in_document_table_attributes(docs)
augment_docs_with_tracking_info(docs, request.user)
augment_docs_and_user_with_user_info(docs, request.user)
meta = {}

View file

@ -58,12 +58,12 @@ import debug # pyflakes:ignore
from ietf.doc.models import ( Document, DocAlias, DocHistory, DocEvent, BallotDocEvent,
ConsensusDocEvent, NewRevisionDocEvent, TelechatDocEvent, WriteupDocEvent,
IESG_BALLOT_ACTIVE_STATES, STATUSCHANGE_RELATIONS )
from ietf.doc.utils import ( add_links_in_new_revision_events, augment_events_with_revision,
can_adopt_draft, can_unadopt_draft, get_chartering_type, get_tags_for_stream_id,
needed_ballot_positions, nice_consensus, prettify_std_name, update_telechat, has_same_ballot,
get_initial_notify, make_notify_changed_event, make_rev_history, default_consensus,
add_events_message_info, get_unicode_document_content, build_doc_meta_block)
from ietf.community.utils import augment_docs_with_tracking_info
from ietf.doc.utils import (add_links_in_new_revision_events, augment_events_with_revision,
can_adopt_draft, can_unadopt_draft, get_chartering_type, get_tags_for_stream_id,
needed_ballot_positions, nice_consensus, prettify_std_name, update_telechat, has_same_ballot,
get_initial_notify, make_notify_changed_event, make_rev_history, default_consensus,
add_events_message_info, get_unicode_document_content, build_doc_meta_block,
augment_docs_and_user_with_user_info)
from ietf.group.models import Role, Group
from ietf.group.utils import can_manage_group_type, can_manage_materials, group_features_role_filter
from ietf.ietfauth.utils import ( has_role, is_authorized_in_doc_stream, user_is_person,
@ -388,7 +388,7 @@ def document_main(request, name, rev=None):
elif can_edit_stream_info and (iesg_state.slug in ('idexists','watching')):
actions.append(("Submit to IESG for Publication", urlreverse('ietf.doc.views_draft.to_iesg', kwargs=dict(name=doc.name))))
augment_docs_with_tracking_info([doc], request.user)
augment_docs_and_user_with_user_info([doc], request.user)
replaces = [d.name for d in doc.related_that_doc("replaces")]
replaced_by = [d.name for d in doc.related_that("replaces")]

View file

@ -5,14 +5,17 @@
from __future__ import absolute_import, print_function, unicode_literals
import io
import json
import os
import datetime
import requests
import email.utils
from django.utils.http import is_safe_url
import debug # pyflakes:ignore
from django.http import HttpResponseForbidden, JsonResponse
from django.http import HttpResponseForbidden, JsonResponse, HttpResponse, HttpResponseRedirect
from django.shortcuts import render, get_object_or_404, redirect
from django import forms
from django.conf import settings
@ -27,7 +30,7 @@ from ietf.doc.models import (Document, NewRevisionDocEvent, State, DocAlias,
from ietf.name.models import ReviewRequestStateName, ReviewAssignmentStateName, ReviewResultName, \
DocTypeName, ReviewTypeName
from ietf.person.models import Person
from ietf.review.models import ReviewRequest, ReviewAssignment
from ietf.review.models import ReviewRequest, ReviewAssignment, ReviewWish
from ietf.group.models import Group
from ietf.ietfauth.utils import is_authorized_in_doc_stream, user_is_person, has_role
from ietf.message.models import Message
@ -967,3 +970,64 @@ def edit_deadline(request, name, request_id):
'review_req': review_req,
'form' : form,
})
class ReviewWishAddForm(forms.Form):
team = forms.ModelChoiceField(queryset=Group.objects.filter(reviewteamsettings__isnull=False),
widget=forms.RadioSelect, empty_label=None, required=True)
def __init__(self, user, doc, *args, **kwargs):
super(ReviewWishAddForm, self).__init__(*args, **kwargs)
self.person = get_object_or_404(Person, user=user)
self.doc = doc
self.fields['team'].queryset = self.fields['team'].queryset.filter(role__person=self.person,
role__name='reviewer')
if len(self.fields['team'].queryset) == 1:
self.team = self.fields['team'].queryset.get()
del self.fields['team']
def save(self):
team = self.team if hasattr(self, 'team') else self.cleaned_data['team']
ReviewWish.objects.get_or_create(person=self.person, team=team, doc=self.doc)
@login_required
def review_wish_add(request, name):
doc = get_object_or_404(Document, docalias__name=name)
if request.method == "POST":
form = ReviewWishAddForm(request.user, doc, request.POST)
if form.is_valid():
form.save()
return _generate_ajax_or_redirect_response(request, doc)
else:
form = ReviewWishAddForm(request.user, doc)
return render(request, "doc/review/review_wish_add.html", {
"doc": doc,
"form": form,
})
@login_required
def review_wishes_remove(request, name):
doc = get_object_or_404(Document, docalias__name=name)
person = get_object_or_404(Person, user=request.user)
if request.method == "POST":
ReviewWish.objects.filter(person=person, doc=doc).delete()
return _generate_ajax_or_redirect_response(request, doc)
return render(request, "doc/review/review_wishes_remove.html", {
"name": doc.name,
})
def _generate_ajax_or_redirect_response(request, doc):
redirect_url = request.GET.get('next')
url_is_safe = is_safe_url(url=redirect_url, allowed_hosts=request.get_host(),
require_https=request.is_secure())
if request.is_ajax():
return HttpResponse(json.dumps({'success': True}), content_type='application/json')
elif url_is_safe:
return HttpResponseRedirect(redirect_url)
else:
return HttpResponseRedirect(doc.get_absolute_url())

View file

@ -98,10 +98,9 @@ $(document).ready(function () {
updateAdvanced();
}
// search results
$('.track-untrack-doc').click(function(e) {
e.preventDefault();
$('.review-wish-add-remove-doc.ajax, .track-untrack-doc').click(function(e) {
e.preventDefault();
var trigger = $(this);
$.ajax({
url: trigger.attr('href'),
@ -109,13 +108,21 @@ $(document).ready(function () {
cache: false,
dataType: 'json',
success: function(response){
if (response.success) {
trigger.parent().find(".tooltip").remove();
trigger.addClass("hide");
trigger.parent().find(".track-untrack-doc").not(trigger).removeClass("hide");
if (response.success) {
trigger.parent().find(".tooltip").remove();
trigger.addClass("hide");
var target_unhide = null;
if(trigger.hasClass('review-wish-add-remove-doc')) {
target_unhide = '.review-wish-add-remove-doc';
} else if(trigger.hasClass('track-untrack-doc')) {
target_unhide = '.track-untrack-doc';
}
}
});
if(target_unhide) {}
trigger.parent().find(target_unhide).not(trigger).removeClass("hide");
}
}
});
});
});

View file

@ -587,6 +587,10 @@
<a class="btn btn-default btn-xs track-untrack-doc {% if not doc.tracked_in_personal_community_list %}hide{% endif %}" href="{% url "ietf.community.views.untrack_document" username=user.username name=doc.name %}" title="Remove from your personal ID list"><span class="fa fa-bookmark"></span> Untrack</a>
<a class="btn btn-default btn-xs track-untrack-doc {% if doc.tracked_in_personal_community_list %}hide{% endif %}" href="{% url "ietf.community.views.track_document" username=user.username name=doc.name %}" title="Add to your personal ID list"><span class="fa fa-bookmark-o"></span> Track</a>
{% endif %}
{% if user.review_teams %}
<a class="btn btn-default btn-xs review-wish-add-remove-doc ajax {% if not doc.has_review_wish %}hide{% endif %}" href="{% url "ietf.doc.views_review.review_wishes_remove" name=doc.name %}?next={{ request.get_full_path|urlencode }}" title="Remove from your review wishes for all teams"><span class="fa fa-comments"></span> Remove review wishes</a>
<a class="btn btn-default btn-xs review-wish-add-remove-doc {% if user.review_teams|length_is:"1" %}ajax {% endif %}{% if doc.has_review_wish %}hide{% endif %}" href="{% url "ietf.doc.views_review.review_wish_add" name=doc.name %}?next={{ request.get_full_path|urlencode }}" title="Add to your review wishes"><span class="fa fa-comments-o"></span> Add review wish</a>
{% endif %}
{% if can_edit and iesg_state.slug != 'idexists' %}
<a class="btn btn-default btn-xs" href="{% url 'ietf.doc.views_ballot.lastcalltext' name=doc.name %}">Last call text</a>

View file

@ -0,0 +1,23 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2016, All Rights Reserved #}
{% load origin bootstrap3 static %}
{% block title %}Add {{ doc.name }} to your review wishes{% endblock %}
{% block content %}
{% origin %}
<h1>Add {{ doc.name }} to your review wishes
</h1>
<p>You are a reviewer for multiple teams, and need to select a team first.</p>
<form class="form-horizontal" method="post">
{% csrf_token %}
{% bootstrap_form form layout="horizontal" %}
{% buttons %}
<button type="submit" class="btn btn-primary">Add to review wishes</button>
{% endbuttons %}
</form>
{% endblock %}

View file

@ -0,0 +1,20 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2016, All Rights Reserved #}
{% load origin bootstrap3 static %}
{% block title %}Remove {{ doc.name }} from your review wishes{% endblock %}
{% block content %}
{% origin %}
<h1>Remove {{ doc.name }} from your review wishes
</h1>
<form class="form-horizontal" method="post">
{% csrf_token %}
{% buttons %}
<button type="submit" class="btn btn-primary">Remove from review wishes</button>
{% endbuttons %}
</form>
{% endblock %}

View file

@ -21,6 +21,16 @@
</a>
<br>
{% endif %}
{% if user.review_teams %}
<a class="review-wish-add-remove-doc ajax {% if not doc.has_review_wish %}hide{% endif %}" href="{% url "ietf.doc.views_review.review_wishes_remove" name=doc.name %}?next={{ request.get_full_path|urlencode }}" title="Remove from your review wishes for all teams">
<span class="fa fa-comments"></span>
</a>
<a class="review-wish-add-remove-doc {% if user.review_teams|length_is:"1" %}ajax {% endif %}{% if doc.has_review_wish %}hide{% endif %}" href="{% url "ietf.doc.views_review.review_wish_add" name=doc.name %}?next={{ request.get_full_path|urlencode }}" title="Add to your review wishes">
<span class="fa fa-comments-o"></span>
</a>
<br>
{% endif %}
{% for session in doc.sessions %}
<a href="{% url 'ietf.meeting.views.ical_agenda' num=session.meeting.number session_id=session.id %}"
title="Calendar entry: document is on the agenda for {{ session.group.acronym }}@{{ session.meeting }}">