feat: Reclassify nomcom feedback (#6002)
* fix: Clean up view_feedback_pending - Remove "Unclassified" column header, which caused misalignment in the table body. - Show the message author - previously displayed as `(None)`. * feat: Reclassify nomcom feedback (#4669) - There's a new `Chair/Advisor Tasks` menu item `Reclassify feedback`. - I overloaded `view_feedback*` URLs with a `?reclassify` parameter. - This adds a checkbox to each feedback message, and a `Reclassify` button at the bottom of each feedback page. - "Reclassifying" basically de-classifies the feedback, and punts it back to the "Pending emails" view for reclassification. - If a feedback has been applied to multiple nominees, declassifying it from one nominee removes it from all. * fix: Remove unused local variables * fix: Fix some missing and mis-nested html * test: Add tests for reclassifying feedback * refactor: Substantial redesign of feedback reclassification - Break out reclassify_feedback* as their own URLs and views, and revert changes to view_feedback*.html. - Replace checkboxes with a Reclassify button on each message. * fix: Remember to clear the feedback associations when reclassifying * feat: Add an 'Overcome by events' feedback type * refactor: When invoking reclassification from a view-feedback page, load the corresponding reclassify-feedback page * fix: De-conflict migration with 0004_statements Also change the coding style to match, and add a reverse migration. * fix: Fix a test case to account for new feedback type * fix: 842e730 broke the Back button * refactor: Reclassify feedback directly instead of putting it back in the work queue * fix: Adjust tests to new workflow * refactor: Further refine reclassification to avoid redirects * refactor: Impose a FeedbackTypeName ordering Also add FeedbackTypeName.legend field, rather than synthesizing it every time we classify or reclassify feedback. In the reclassification forms, only show the relevant feedback types. * refactor: Merge reclassify_feedback_* back into view_feedback_* This means the "Reclassify" button is always present, but eliminates some complexity. * refactor: Add filter(used=True) on FeedbackTypeName querysets * refactor: Add the new FeedbackTypeName to the reclassification success message * fix: Secure reclassification against rogue nomcom members
This commit is contained in:
parent
b327a27736
commit
06c9f06d55
|
@ -11113,8 +11113,9 @@
|
|||
{
|
||||
"fields": {
|
||||
"desc": "",
|
||||
"legend": "C",
|
||||
"name": "Comment",
|
||||
"order": 0,
|
||||
"order": 1,
|
||||
"used": true
|
||||
},
|
||||
"model": "name.feedbacktypename",
|
||||
|
@ -11123,8 +11124,9 @@
|
|||
{
|
||||
"fields": {
|
||||
"desc": "",
|
||||
"legend": "J",
|
||||
"name": "Junk",
|
||||
"order": 0,
|
||||
"order": 5,
|
||||
"used": true
|
||||
},
|
||||
"model": "name.feedbacktypename",
|
||||
|
@ -11133,8 +11135,9 @@
|
|||
{
|
||||
"fields": {
|
||||
"desc": "",
|
||||
"legend": "N",
|
||||
"name": "Nomination",
|
||||
"order": 0,
|
||||
"order": 2,
|
||||
"used": true
|
||||
},
|
||||
"model": "name.feedbacktypename",
|
||||
|
@ -11143,8 +11146,20 @@
|
|||
{
|
||||
"fields": {
|
||||
"desc": "",
|
||||
"legend": "O",
|
||||
"name": "Overcome by events",
|
||||
"order": 4,
|
||||
"used": true
|
||||
},
|
||||
"model": "name.feedbacktypename",
|
||||
"pk": "obe"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "",
|
||||
"legend": "Q",
|
||||
"name": "Questionnaire response",
|
||||
"order": 0,
|
||||
"order": 3,
|
||||
"used": true
|
||||
},
|
||||
"model": "name.feedbacktypename",
|
||||
|
@ -11153,8 +11168,9 @@
|
|||
{
|
||||
"fields": {
|
||||
"desc": "",
|
||||
"legend": "R",
|
||||
"name": "Read",
|
||||
"order": 0,
|
||||
"order": 6,
|
||||
"used": true
|
||||
},
|
||||
"model": "name.feedbacktypename",
|
||||
|
|
20
ietf/name/migrations/0005_feedbacktypename_schema.py
Normal file
20
ietf/name/migrations/0005_feedbacktypename_schema.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Copyright The IETF Trust 2023, All Rights Reserved
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("name", "0004_statements"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="FeedbackTypeName",
|
||||
name="legend",
|
||||
field=models.CharField(
|
||||
default="",
|
||||
help_text="One-character legend for feedback classification form",
|
||||
max_length=1,
|
||||
),
|
||||
),
|
||||
]
|
36
ietf/name/migrations/0006_feedbacktypename_data.py
Normal file
36
ietf/name/migrations/0006_feedbacktypename_data.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Copyright The IETF Trust 2023, All Rights Reserved
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
def forward(apps, schema_editor):
|
||||
FeedbackTypeName = apps.get_model("name", "FeedbackTypeName")
|
||||
FeedbackTypeName.objects.create(slug="obe", name="Overcome by events")
|
||||
for slug, legend, order in (
|
||||
('comment', 'C', 1),
|
||||
('nomina', 'N', 2),
|
||||
('questio', 'Q', 3),
|
||||
('obe', 'O', 4),
|
||||
('junk', 'J', 5),
|
||||
('read', 'R', 6),
|
||||
):
|
||||
ft = FeedbackTypeName.objects.get(slug=slug)
|
||||
ft.legend = legend
|
||||
ft.order = order
|
||||
ft.save()
|
||||
|
||||
def reverse(apps, schema_editor):
|
||||
FeedbackTypeName = apps.get_model("name", "FeedbackTypeName")
|
||||
FeedbackTypeName.objects.filter(slug="obe").delete()
|
||||
for ft in FeedbackTypeName.objects.all():
|
||||
ft.legend = ""
|
||||
ft.order = 0
|
||||
ft.save()
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("name", "0005_feedbacktypename_schema"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(forward, reverse),
|
||||
]
|
|
@ -94,6 +94,7 @@ class NomineePositionStateName(NameModel):
|
|||
"""Status of a candidate for a position: None, Accepted, Declined"""
|
||||
class FeedbackTypeName(NameModel):
|
||||
"""Type of feedback: questionnaires, nominations, comments"""
|
||||
legend = models.CharField(max_length=1, default="", help_text="One-character legend for feedback classification form")
|
||||
class DBTemplateTypeName(NameModel):
|
||||
"""reStructuredText, Plain, Django"""
|
||||
class DraftSubmissionStateName(NameModel):
|
||||
|
|
|
@ -653,7 +653,7 @@ class PrivateKeyForm(forms.Form):
|
|||
|
||||
class PendingFeedbackForm(forms.ModelForm):
|
||||
|
||||
type = forms.ModelChoiceField(queryset=FeedbackTypeName.objects.all().order_by('pk'), widget=forms.RadioSelect, empty_label='Unclassified', required=False)
|
||||
type = forms.ModelChoiceField(queryset=FeedbackTypeName.objects.all(), widget=forms.RadioSelect, empty_label='Unclassified', required=False)
|
||||
|
||||
class Meta:
|
||||
model = Feedback
|
||||
|
|
|
@ -1450,7 +1450,7 @@ class FeedbackIndexTests(TestCase):
|
|||
self.assertEqual(response.status_code,200)
|
||||
q = PyQuery(response.content)
|
||||
r = q('tfoot').eq(0).find('td').contents()
|
||||
self.assertEqual([a.strip() for a in r], ['1', '1', '1'])
|
||||
self.assertEqual([a.strip() for a in r], ['1', '1', '1', '0'])
|
||||
|
||||
class FeedbackLastSeenTests(TestCase):
|
||||
|
||||
|
@ -2863,3 +2863,92 @@ class VolunteerDecoratorUnitTests(TestCase):
|
|||
self.assertEqual(v.qualifications,'path_2')
|
||||
if v.person == author_person:
|
||||
self.assertEqual(v.qualifications,'path_3')
|
||||
|
||||
class ReclassifyFeedbackTests(TestCase):
|
||||
"""Tests for feedback reclassification"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
setup_test_public_keys_dir(self)
|
||||
nomcom_test_data()
|
||||
self.nc = NomComFactory.create(**nomcom_kwargs_for_year())
|
||||
self.chair = self.nc.group.role_set.filter(name='chair').first().person
|
||||
self.member = self.nc.group.role_set.filter(name='member').first().person
|
||||
self.nominee = self.nc.nominee_set.order_by('pk').first()
|
||||
self.position = self.nc.position_set.first()
|
||||
self.topic = self.nc.topic_set.first()
|
||||
|
||||
def tearDown(self):
|
||||
teardown_test_public_keys_dir(self)
|
||||
super().tearDown()
|
||||
|
||||
def test_reclassify_feedback_nominee(self):
|
||||
fb = FeedbackFactory.create(nomcom=self.nc,type_id='comment')
|
||||
fb.positions.add(self.position)
|
||||
fb.nominees.add(self.nominee)
|
||||
fb.save()
|
||||
self.assertEqual(Feedback.objects.comments().count(), 1)
|
||||
|
||||
url = reverse('ietf.nomcom.views.view_feedback_nominee', kwargs={'year':self.nc.year(), 'nominee_id':self.nominee.id})
|
||||
login_testing_unauthorized(self,self.member.user.username,url)
|
||||
provide_private_key_to_test_client(self)
|
||||
response = self.client.post(url, {'feedback_id': fb.id, 'type': 'obe'})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
self.client.logout()
|
||||
self.client.login(username=self.chair.user.username, password=self.chair.user.username + "+password")
|
||||
provide_private_key_to_test_client(self)
|
||||
|
||||
response = self.client.post(url, {'feedback_id': fb.id, 'type': 'obe'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
fb = Feedback.objects.get(id=fb.id)
|
||||
self.assertEqual(fb.type_id,'obe')
|
||||
self.assertEqual(Feedback.objects.comments().count(), 0)
|
||||
self.assertEqual(Feedback.objects.filter(type='obe').count(), 1)
|
||||
|
||||
def test_reclassify_feedback_topic(self):
|
||||
fb = FeedbackFactory.create(nomcom=self.nc,type_id='comment')
|
||||
fb.topics.add(self.topic)
|
||||
fb.save()
|
||||
self.assertEqual(Feedback.objects.comments().count(), 1)
|
||||
|
||||
url = reverse('ietf.nomcom.views.view_feedback_topic', kwargs={'year':self.nc.year(), 'topic_id':self.topic.id})
|
||||
login_testing_unauthorized(self,self.member.user.username,url)
|
||||
provide_private_key_to_test_client(self)
|
||||
response = self.client.post(url, {'feedback_id': fb.id, 'type': 'unclassified'})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
self.client.logout()
|
||||
self.client.login(username=self.chair.user.username, password=self.chair.user.username + "+password")
|
||||
provide_private_key_to_test_client(self)
|
||||
|
||||
response = self.client.post(url, {'feedback_id': fb.id, 'type': 'unclassified'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
fb = Feedback.objects.get(id=fb.id)
|
||||
self.assertEqual(fb.type_id,None)
|
||||
self.assertEqual(Feedback.objects.comments().count(), 0)
|
||||
self.assertEqual(Feedback.objects.filter(type=None).count(), 1)
|
||||
|
||||
def test_reclassify_feedback_unrelated(self):
|
||||
fb = FeedbackFactory(nomcom=self.nc, type_id='read')
|
||||
self.assertEqual(Feedback.objects.filter(type='read').count(), 1)
|
||||
|
||||
url = reverse('ietf.nomcom.views.view_feedback_unrelated', kwargs={'year':self.nc.year()})
|
||||
login_testing_unauthorized(self,self.member.user.username,url)
|
||||
provide_private_key_to_test_client(self)
|
||||
response = self.client.post(url, {'feedback_id': fb.id, 'type': 'junk'})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
self.client.logout()
|
||||
self.client.login(username=self.chair.user.username, password=self.chair.user.username + "+password")
|
||||
provide_private_key_to_test_client(self)
|
||||
|
||||
response = self.client.post(url, {'feedback_id': fb.id, 'type': 'junk'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
fb = Feedback.objects.get(id=fb.id)
|
||||
self.assertEqual(fb.type_id, 'junk')
|
||||
self.assertEqual(Feedback.objects.filter(type='read').count(), 0)
|
||||
self.assertEqual(Feedback.objects.filter(type='junk').count(), 1)
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
# Copyright The IETF Trust 2012-2020, All Rights Reserved
|
||||
# Copyright The IETF Trust 2012-2023, All Rights Reserved
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import datetime
|
||||
import re
|
||||
from collections import OrderedDict, Counter
|
||||
from collections import Counter
|
||||
import csv
|
||||
import hmac
|
||||
|
||||
|
@ -14,7 +14,7 @@ from django.contrib.auth.decorators import login_required
|
|||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||
from django.forms.models import modelformset_factory, inlineformset_factory
|
||||
from django.http import Http404, HttpResponseRedirect, HttpResponse
|
||||
from django.http import Http404, HttpResponseRedirect, HttpResponse, HttpResponseForbidden
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
|
@ -767,7 +767,6 @@ def process_nomination_status(request, year, nominee_position_id, state, date, h
|
|||
'selected': 'feedback',
|
||||
'form': form })
|
||||
|
||||
|
||||
@role_required("Nomcom")
|
||||
@nomcom_private_key_required
|
||||
def view_feedback(request, year):
|
||||
|
@ -775,7 +774,7 @@ def view_feedback(request, year):
|
|||
nominees = Nominee.objects.get_by_nomcom(nomcom).not_duplicated().distinct()
|
||||
independent_feedback_types = []
|
||||
nominee_feedback_types = []
|
||||
for ft in FeedbackTypeName.objects.all():
|
||||
for ft in FeedbackTypeName.objects.filter(used=True):
|
||||
if ft.slug in settings.NOMINEE_FEEDBACK_TYPES:
|
||||
nominee_feedback_types.append(ft)
|
||||
else:
|
||||
|
@ -838,7 +837,8 @@ def view_feedback(request, year):
|
|||
'topics_feedback': topics_feedback,
|
||||
'independent_feedback': independent_feedback,
|
||||
'nominees_feedback': nominees_feedback,
|
||||
'nomcom': nomcom})
|
||||
'nomcom': nomcom,
|
||||
})
|
||||
|
||||
|
||||
@role_required("Nomcom Chair", "Nomcom Advisor")
|
||||
|
@ -924,23 +924,13 @@ def view_feedback_pending(request, year):
|
|||
formset = FeedbackFormSet(queryset=feedback_page.object_list)
|
||||
for form in formset.forms:
|
||||
form.set_nomcom(nomcom, request.user)
|
||||
type_dict = OrderedDict()
|
||||
for t in FeedbackTypeName.objects.all().order_by('pk'):
|
||||
rest = t.name
|
||||
slug = rest[0]
|
||||
rest = rest[1:]
|
||||
while slug in type_dict and rest:
|
||||
slug = rest[0]
|
||||
rest = rest[1]
|
||||
type_dict[slug] = t
|
||||
return render(request, 'nomcom/view_feedback_pending.html',
|
||||
{'year': year,
|
||||
'selected': 'feedback_pending',
|
||||
'formset': formset,
|
||||
'extra_step': extra_step,
|
||||
'type_dict': type_dict,
|
||||
'extra_ids': extra_ids,
|
||||
'types': FeedbackTypeName.objects.all().order_by('pk'),
|
||||
'types': FeedbackTypeName.objects.filter(used=True),
|
||||
'nomcom': nomcom,
|
||||
'is_chair_task' : True,
|
||||
'page': feedback_page,
|
||||
|
@ -951,22 +941,59 @@ def view_feedback_pending(request, year):
|
|||
@nomcom_private_key_required
|
||||
def view_feedback_unrelated(request, year):
|
||||
nomcom = get_nomcom_by_year(year)
|
||||
|
||||
if request.method == 'POST':
|
||||
if not nomcom.group.has_role(request.user, ['chair','advisor']):
|
||||
return HttpResponseForbidden('Restricted to roles: Nomcom Chair, Nomcom Advisor')
|
||||
feedback_id = request.POST.get('feedback_id', None)
|
||||
feedback = get_object_or_404(Feedback, id=feedback_id)
|
||||
type = request.POST.get('type', None)
|
||||
if type:
|
||||
if type == 'unclassified':
|
||||
feedback.type = None
|
||||
messages.success(request, 'The selected feedback has been de-classified. Please reclassify it in the Pending emails tab.')
|
||||
else:
|
||||
feedback.type = FeedbackTypeName.objects.get(slug=type)
|
||||
messages.success(request, f'The selected feedback has been reclassified as {feedback.type.name}.')
|
||||
feedback.save()
|
||||
else:
|
||||
return render(request, 'nomcom/view_feedback_unrelated.html',
|
||||
{'year': year,
|
||||
'nomcom': nomcom,
|
||||
'feedback_types': FeedbackTypeName.objects.filter(used=True).exclude(slug__in=settings.NOMINEE_FEEDBACK_TYPES),
|
||||
'reclassify_feedback': feedback,
|
||||
'is_chair_task' : True,
|
||||
})
|
||||
|
||||
feedback_types = []
|
||||
for ft in FeedbackTypeName.objects.exclude(slug__in=settings.NOMINEE_FEEDBACK_TYPES):
|
||||
for ft in FeedbackTypeName.objects.filter(used=True).exclude(slug__in=settings.NOMINEE_FEEDBACK_TYPES):
|
||||
feedback_types.append({'ft': ft,
|
||||
'feedback': ft.feedback_set.get_by_nomcom(nomcom)})
|
||||
|
||||
return render(request, 'nomcom/view_feedback_unrelated.html',
|
||||
{'year': year,
|
||||
'selected': 'view_feedback',
|
||||
'feedback_types': feedback_types,
|
||||
'nomcom': nomcom})
|
||||
'nomcom': nomcom,
|
||||
})
|
||||
|
||||
@role_required("Nomcom")
|
||||
@nomcom_private_key_required
|
||||
def view_feedback_topic(request, year, topic_id):
|
||||
nomcom = get_nomcom_by_year(year)
|
||||
# At present, the only feedback type for topics is 'comment'.
|
||||
# Reclassifying from 'comment' to 'comment' is a no-op,
|
||||
# so the only meaningful action is to de-classify it.
|
||||
if request.method == 'POST':
|
||||
nomcom = get_nomcom_by_year(year)
|
||||
if not nomcom.group.has_role(request.user, ['chair','advisor']):
|
||||
return HttpResponseForbidden('Restricted to roles: Nomcom Chair, Nomcom Advisor')
|
||||
feedback_id = request.POST.get('feedback_id', None)
|
||||
feedback = get_object_or_404(Feedback, id=feedback_id)
|
||||
feedback.type = None
|
||||
feedback.topics.clear()
|
||||
feedback.save()
|
||||
messages.success(request, 'The selected feedback has been de-classified. Please reclassify it in the Pending emails tab.')
|
||||
|
||||
topic = get_object_or_404(Topic, id=topic_id)
|
||||
nomcom = get_nomcom_by_year(year)
|
||||
feedback_types = FeedbackTypeName.objects.filter(slug__in=['comment',])
|
||||
|
||||
last_seen = TopicFeedbackLastSeen.objects.filter(reviewer=request.user.person,topic=topic).first()
|
||||
|
@ -978,18 +1005,42 @@ def view_feedback_topic(request, year, topic_id):
|
|||
|
||||
return render(request, 'nomcom/view_feedback_topic.html',
|
||||
{'year': year,
|
||||
'selected': 'view_feedback',
|
||||
'topic': topic,
|
||||
'feedback_types': feedback_types,
|
||||
'last_seen_time' : last_seen_time,
|
||||
'nomcom': nomcom})
|
||||
'nomcom': nomcom,
|
||||
})
|
||||
|
||||
@role_required("Nomcom")
|
||||
@nomcom_private_key_required
|
||||
def view_feedback_nominee(request, year, nominee_id):
|
||||
nomcom = get_nomcom_by_year(year)
|
||||
nominee = get_object_or_404(Nominee, id=nominee_id)
|
||||
feedback_types = FeedbackTypeName.objects.filter(slug__in=settings.NOMINEE_FEEDBACK_TYPES)
|
||||
feedback_types = FeedbackTypeName.objects.filter(used=True, slug__in=settings.NOMINEE_FEEDBACK_TYPES)
|
||||
|
||||
if request.method == 'POST':
|
||||
if not nomcom.group.has_role(request.user, ['chair','advisor']):
|
||||
return HttpResponseForbidden('Restricted to roles: Nomcom Chair, Nomcom Advisor')
|
||||
feedback_id = request.POST.get('feedback_id', None)
|
||||
feedback = get_object_or_404(Feedback, id=feedback_id)
|
||||
type = request.POST.get('type', None)
|
||||
if type:
|
||||
if type == 'unclassified':
|
||||
feedback.type = None
|
||||
feedback.nominees.clear()
|
||||
messages.success(request, 'The selected feedback has been de-classified. Please reclassify it in the Pending emails tab.')
|
||||
else:
|
||||
feedback.type = FeedbackTypeName.objects.get(slug=type)
|
||||
messages.success(request, f'The selected feedback has been reclassified as {feedback.type.name}.')
|
||||
feedback.save()
|
||||
else:
|
||||
return render(request, 'nomcom/view_feedback_nominee.html',
|
||||
{'year': year,
|
||||
'nomcom': nomcom,
|
||||
'feedback_types': feedback_types,
|
||||
'reclassify_feedback': feedback,
|
||||
'is_chair_task': True,
|
||||
})
|
||||
|
||||
last_seen = FeedbackLastSeen.objects.filter(reviewer=request.user.person,nominee=nominee).first()
|
||||
last_seen_time = (last_seen and last_seen.time) or datetime.datetime(year=1, month=1, day=1, tzinfo=datetime.timezone.utc)
|
||||
|
@ -1000,11 +1051,11 @@ def view_feedback_nominee(request, year, nominee_id):
|
|||
|
||||
return render(request, 'nomcom/view_feedback_nominee.html',
|
||||
{'year': year,
|
||||
'selected': 'view_feedback',
|
||||
'nominee': nominee,
|
||||
'feedback_types': feedback_types,
|
||||
'last_seen_time' : last_seen_time,
|
||||
'nomcom': nomcom})
|
||||
'nomcom': nomcom,
|
||||
})
|
||||
|
||||
|
||||
@role_required("Nomcom Chair", "Nomcom Advisor")
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright The IETF Trust 2007-2022, All Rights Reserved
|
||||
# Copyright The IETF Trust 2007-2023, All Rights Reserved
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
|
@ -802,7 +802,7 @@ NOMCOM_PUBLIC_KEYS_DIR = '/a/www/nomcom/public_keys/'
|
|||
NOMCOM_FROM_EMAIL = 'nomcom-chair-{year}@ietf.org'
|
||||
OPENSSL_COMMAND = '/usr/bin/openssl'
|
||||
DAYS_TO_EXPIRE_NOMINATION_LINK = ''
|
||||
NOMINEE_FEEDBACK_TYPES = ['comment', 'questio', 'nomina']
|
||||
NOMINEE_FEEDBACK_TYPES = ['comment', 'questio', 'nomina', 'obe']
|
||||
|
||||
# SlideSubmission settings
|
||||
SLIDE_STAGING_PATH = '/a/www/www6s/staging/'
|
||||
|
|
103
ietf/templates/nomcom/reclassify_feedback_item.html
Normal file
103
ietf/templates/nomcom/reclassify_feedback_item.html
Normal file
|
@ -0,0 +1,103 @@
|
|||
{# Copyright The IETF Trust 2023, All Rights Reserved #}
|
||||
{% load nomcom_tags textfilters %}
|
||||
<h2 class="mt-3">Reclassify feedback item</h2>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Code</th>
|
||||
<th scope="col">Explanation</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">U</th>
|
||||
<td>Unclassified</td>
|
||||
</tr>
|
||||
{% for ft in feedback_types %}
|
||||
<tr>
|
||||
<th scope="row">{{ ft.legend }}</th>
|
||||
<td>{{ ft.name }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col" class="text-center" title="Unclassified">U</th>
|
||||
{% for ft in feedback_types %}
|
||||
<th scope="col" class="text-center" title="{{ ft.name }}">{{ ft.legend }}</th>
|
||||
{% endfor %}
|
||||
<th scope="col">Author</th>
|
||||
<th scope="col">Subject</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<!-- [html-validate-disable-block input-missing-label -- labelled via aria-label] -->
|
||||
<td>{{ reclassify_feedback.time|date:"r" }}</td>
|
||||
<td class="text-center">
|
||||
<input type="radio"
|
||||
class="form-check-input"
|
||||
name="type"
|
||||
value="unclassified"
|
||||
id="unclassified"
|
||||
aria-label="Unclassified"
|
||||
title="Unclassified">
|
||||
</td>
|
||||
{% for ft in feedback_types %}
|
||||
<td class="text-center">
|
||||
<input type="radio"
|
||||
class="form-check-input"
|
||||
name="type"
|
||||
value="{{ ft.slug }}"
|
||||
id="{{ ft.name|slugify }}"
|
||||
aria-label="{{ ft.name }}"
|
||||
{% if reclassify_feedback.type == t %}checked{% endif %}
|
||||
title="{{ ft.name }}">
|
||||
</td>
|
||||
{% endfor %}
|
||||
<td>{{ reclassify_feedback.author }}</td>
|
||||
<td>{{ reclassify_feedback.subject }}</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#modal{{ reclassify_feedback.id }}">
|
||||
View
|
||||
</button>
|
||||
<div class="modal fade"
|
||||
id="modal{{ reclassify_feedback.id }}"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
aria-labelledby="label{{ reclassify_feedback.id }}"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-scrollable modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<p class="h5 modal-title" id="label{{ reclassify_feedback.id }}">{{ reclassify_feedback.subject }}</p>
|
||||
<button type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre>{% decrypt reclassify_feedback.comments request year 1 %}</pre>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<input type="hidden" name="feedback_id" value="{{ reclassify_feedback.id }}">
|
||||
<button class="btn btn-primary" type="submit">Classify</button>
|
||||
</form>
|
|
@ -1,10 +1,13 @@
|
|||
{% extends "nomcom/nomcom_private_base.html" %}
|
||||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{# Copyright The IETF Trust 2015-2023, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
{% load nomcom_tags textfilters %}
|
||||
{% block subtitle %}- View feedback about {{ nominee.email.person.name }}{% endblock %}
|
||||
{% block nomcom_content %}
|
||||
{% origin %}
|
||||
{% origin %}
|
||||
{% if reclassify_feedback %}
|
||||
{% include "nomcom/reclassify_feedback_item.html" %}
|
||||
{% else %}
|
||||
<h2>Feedback about {{ nominee }}</h2>
|
||||
<ul class="nav nav-tabs my-3" role="tablist">
|
||||
{% for ft in feedback_types %}
|
||||
|
@ -81,6 +84,19 @@
|
|||
<dd class="col-sm-10 pasted">
|
||||
<pre>{% decrypt feedback.comments request year 1 %}</pre>
|
||||
</dd>
|
||||
{% if user|is_chair_or_advisor:year %}
|
||||
<dt class="col-sm-2">
|
||||
<form id="reclassify-{{ feedback.id }}" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="feedback_id" value="{{ feedback.id }}">
|
||||
<button class="btn btn-warning btn-sm" type="submit">
|
||||
Reclassify
|
||||
</button>
|
||||
</form>
|
||||
</dt>
|
||||
<dd>
|
||||
</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
{% if not forloop.last %}<hr>{% endif %}
|
||||
{% endif %}
|
||||
|
@ -90,6 +106,7 @@
|
|||
</div>
|
||||
<a class="btn btn-secondary"
|
||||
href="{% url 'ietf.nomcom.views.view_feedback' year %}">Back</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block js %}
|
||||
<script>
|
||||
|
|
|
@ -73,9 +73,9 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for legend, t in type_dict.items %}
|
||||
{% for t in types %}
|
||||
<tr>
|
||||
<th scope="row">{{ legend }}</th>
|
||||
<th scope="row">{{ t.legend }}</th>
|
||||
<td>{{ t.name }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
@ -85,7 +85,7 @@
|
|||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Date</th>
|
||||
{% for legend, t in type_dict.items %}<th scope="col" class="text-center" title="{{ t.name }}">{{ legend }}</th>{% endfor %}
|
||||
{% for t in types %}<th scope="col" class="text-center" title="{{ t.name }}">{{ t.legend }}</th>{% endfor %}
|
||||
<th scope="col">Author</th>
|
||||
<th scope="col">Subject</th>
|
||||
<th scope="col"></th>
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
{% extends "nomcom/nomcom_private_base.html" %}
|
||||
{# Copyright The IETF Trust 2017, All Rights Reserved #}
|
||||
{# Copyright The IETF Trust 2017-2023, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
{% load nomcom_tags textfilters %}
|
||||
{% block subtitle %}- View feedback about {{ topic.subject }}{% endblock %}
|
||||
{% block nomcom_content %}
|
||||
{% origin %}
|
||||
{% origin %}
|
||||
{% if reclassify_feedback %}
|
||||
{% include "nomcom/reclassify_feedback_item.html" %}
|
||||
{% else %}
|
||||
<h2>Feedback about {{ topic.subject }}</h2>
|
||||
<ul class="nav nav-tabs my-3" role="tablist">
|
||||
{% for ft in feedback_types %}
|
||||
|
@ -44,6 +47,19 @@
|
|||
<dd class="col-sm-10 pasted">
|
||||
<pre>{% decrypt feedback.comments request year 1 %}</pre>
|
||||
</dd>
|
||||
{% if user|is_chair_or_advisor:year %}
|
||||
<dt class="col-sm-2">
|
||||
<form id="reclassify-{{ feedback.id }}" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="feedback_id" value="{{ feedback.id }}">
|
||||
<button class="btn btn-warning btn-sm" type="submit">
|
||||
Reclassify
|
||||
</button>
|
||||
</form>
|
||||
</dt>
|
||||
<dd>
|
||||
</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
{% if not forloop.last %}<hr>{% endif %}
|
||||
{% endif %}
|
||||
|
@ -53,6 +69,7 @@
|
|||
</div>
|
||||
<a class="btn btn-secondary"
|
||||
href="{% url 'ietf.nomcom.views.view_feedback' year %}">Back</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block js %}
|
||||
<script>
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
{% extends "nomcom/nomcom_private_base.html" %}
|
||||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{# Copyright The IETF Trust 2015-2023, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
{% load nomcom_tags textfilters %}
|
||||
{% block subtitle %}- View unrelated feedback{% endblock %}
|
||||
{% block nomcom_content %}
|
||||
{% origin %}
|
||||
{% origin %}
|
||||
{% if reclassify_feedback %}
|
||||
{% include "nomcom/reclassify_feedback_item.html" %}
|
||||
{% else %}
|
||||
<h2>Feedback not related to nominees</h2>
|
||||
<ul role="tablist" class="nav nav-tabs my-3">
|
||||
{% for ft in feedback_types %}
|
||||
|
@ -45,6 +48,19 @@
|
|||
<dd class="col-sm-10 pasted">
|
||||
<pre>{% decrypt feedback.comments request year 1 %}</pre>
|
||||
</dd>
|
||||
{% if user|is_chair_or_advisor:year %}
|
||||
<dt class="col-sm-2">
|
||||
<form id="reclassify-{{ feedback.id }}" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="feedback_id" value="{{ feedback.id }}">
|
||||
<button class="btn btn-warning btn-sm" type="submit">
|
||||
Reclassify
|
||||
</button>
|
||||
</form>
|
||||
</dt>
|
||||
<dd>
|
||||
</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
@ -54,6 +70,7 @@
|
|||
<a class="btn btn-secondary"
|
||||
href="{% url 'ietf.nomcom.views.view_feedback' year %}">Back</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block js %}
|
||||
<script>
|
||||
|
|
Loading…
Reference in a new issue