Merged in [16939] from sasha@dashcare.nl:
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.
- Legacy-Id: 16985
Note: SVN reference [16939] has been migrated to Git commit 6e55f26dbd
This commit is contained in:
commit
20eb9d8ac1
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
@ -479,7 +480,7 @@ class ReviewTests(TestCase):
|
|||
login_testing_unauthorized(self, "reviewsecretary", reject_url)
|
||||
r = self.client.get(reject_url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertContains(r, str(assignment.reviewer.person))
|
||||
self.assertContains(r, assignment.reviewer.person.plain_name())
|
||||
self.assertNotContains(r, 'can not be rejected')
|
||||
self.assertContains(r, '<button type="submit"')
|
||||
|
||||
|
@ -1165,3 +1166,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())
|
||||
|
|
|
@ -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/$', views_review.review_wish_add),
|
||||
url(r'^removereviewwishes/$', views_review.review_wishes_remove),
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
@ -906,3 +910,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
|
||||
|
|
|
@ -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 = {}
|
||||
|
||||
|
|
|
@ -58,12 +58,12 @@ import debug # pyflakes:ignore
|
|||
from ietf.doc.models import ( Document, DocAlias, DocHistory, DocEvent, BallotDocEvent,
|
||||
ConsensusDocEvent, NewRevisionDocEvent, TelechatDocEvent, WriteupDocEvent, IanaExpertDocEvent,
|
||||
IESG_BALLOT_ACTIVE_STATES, STATUSCHANGE_RELATIONS )
|
||||
from ietf.doc.utils import ( add_links_in_new_revision_events, augment_events_with_revision,
|
||||
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
|
||||
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,
|
||||
|
@ -390,7 +390,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")]
|
||||
|
|
|
@ -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, Http404
|
||||
from django.http import HttpResponseForbidden, JsonResponse, Http404, 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
|
||||
|
@ -970,3 +973,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())
|
||||
|
|
|
@ -99,9 +99,8 @@ $(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");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -622,6 +622,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>
|
||||
|
|
23
ietf/templates/doc/review/review_wish_add.html
Normal file
23
ietf/templates/doc/review/review_wish_add.html
Normal 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 %}
|
20
ietf/templates/doc/review/review_wishes_remove.html
Normal file
20
ietf/templates/doc/review/review_wishes_remove.html
Normal 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 %}
|
|
@ -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 }}">
|
||||
|
|
Loading…
Reference in a new issue