Make it possible to close nominations without closing feedback. Fixes #2255. Commit ready for merge.

- Legacy-Id: 13399
This commit is contained in:
Robert Sparks 2017-05-22 18:40:05 +00:00
parent 1a18130c57
commit ac8e5f5402
12 changed files with 193 additions and 20 deletions

View file

@ -24,7 +24,7 @@ class NomineePositionAdmin(admin.ModelAdmin):
class PositionAdmin(admin.ModelAdmin): class PositionAdmin(admin.ModelAdmin):
list_display = ('name', 'nomcom', 'is_open') list_display = ('name', 'nomcom', 'is_open', 'accepting_nominations', 'accepting_feedback')
list_filter = ('nomcom',) list_filter = ('nomcom',)

View file

@ -128,6 +128,8 @@ class PositionFactory(factory.DjangoModelFactory):
name = factory.Faker('sentence',nb_words=5) name = factory.Faker('sentence',nb_words=5)
is_open = True is_open = True
accepting_nominations = True
accepting_feedback = True
class NomineeFactory(factory.DjangoModelFactory): class NomineeFactory(factory.DjangoModelFactory):
class Meta: class Meta:

View file

@ -41,7 +41,7 @@ class PositionNomineeField(forms.ChoiceField):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.nomcom = kwargs.pop('nomcom') self.nomcom = kwargs.pop('nomcom')
positions = Position.objects.get_by_nomcom(self.nomcom).opened().order_by('name') positions = Position.objects.get_by_nomcom(self.nomcom).filter(is_open=True).order_by('name')
results = [] results = []
for position in positions: for position in positions:
accepted_nominees = [np.nominee for np in NomineePosition.objects.filter(position=position,state='accepted').exclude(nominee__duplicated__isnull=False)] accepted_nominees = [np.nominee for np in NomineePosition.objects.filter(position=position,state='accepted').exclude(nominee__duplicated__isnull=False)]
@ -62,7 +62,7 @@ class PositionNomineeField(forms.ChoiceField):
return nominee return nominee
(position_id, nominee_id) = nominee.split('_') (position_id, nominee_id) = nominee.split('_')
try: try:
position = Position.objects.get_by_nomcom(self.nomcom).opened().get(id=position_id) position = Position.objects.get_by_nomcom(self.nomcom).filter(is_open=True).get(id=position_id)
except Position.DoesNotExist: except Position.DoesNotExist:
raise forms.ValidationError('Invalid nominee') raise forms.ValidationError('Invalid nominee')
try: try:
@ -82,7 +82,7 @@ class MultiplePositionNomineeField(forms.MultipleChoiceField, PositionNomineeFie
return nominee return nominee
(position_id, nominee_id) = nominee.split('_') (position_id, nominee_id) = nominee.split('_')
try: try:
position = Position.objects.get_by_nomcom(self.nomcom).opened().get(id=position_id) position = Position.objects.get_by_nomcom(self.nomcom).filter(is_open=True).get(id=position_id)
except Position.DoesNotExist: except Position.DoesNotExist:
raise forms.ValidationError('Invalid nominee') raise forms.ValidationError('Invalid nominee')
try: try:
@ -356,7 +356,9 @@ class NominateForm(forms.ModelForm):
self.fields['searched_email'].help_text = 'Search by name or email address. Click <a href="%s">here</a> if the search does not find the candidate you want to nominate.' % reverse(new_person_url_name,kwargs={'year':self.nomcom.year()}) self.fields['searched_email'].help_text = 'Search by name or email address. Click <a href="%s">here</a> if the search does not find the candidate you want to nominate.' % reverse(new_person_url_name,kwargs={'year':self.nomcom.year()})
self.fields['nominator_email'].label = 'Nominator email' self.fields['nominator_email'].label = 'Nominator email'
if self.nomcom: if self.nomcom:
self.fields['position'].queryset = Position.objects.get_by_nomcom(self.nomcom).opened() self.fields['position'].queryset = Position.objects.get_by_nomcom(self.nomcom).filter(is_open=True)
if self.public:
self.fields['position'].queryset = self.fields['position'].queryset.filter(accepting_nominations=True)
self.fields['qualifications'].help_text = self.nomcom.initial_text self.fields['qualifications'].help_text = self.nomcom.initial_text
if not self.public: if not self.public:
@ -454,7 +456,9 @@ class NominateNewPersonForm(forms.ModelForm):
self.fields['nominator_email'].label = 'Nominator email' self.fields['nominator_email'].label = 'Nominator email'
if self.nomcom: if self.nomcom:
self.fields['position'].queryset = Position.objects.get_by_nomcom(self.nomcom).opened() self.fields['position'].queryset = Position.objects.get_by_nomcom(self.nomcom).filter(is_open=True)
if self.public:
self.fields['position'].queryset = self.fields['position'].queryset.filter(accepting_nominations=True)
self.fields['qualifications'].help_text = self.nomcom.initial_text self.fields['qualifications'].help_text = self.nomcom.initial_text
if not self.public: if not self.public:
@ -680,7 +684,7 @@ class PositionForm(forms.ModelForm):
class Meta: class Meta:
model = Position model = Position
fields = ('name', 'is_open') fields = ('name', 'is_open', 'accepting_nominations', 'accepting_feedback')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.nomcom = kwargs.pop('nomcom', None) self.nomcom = kwargs.pop('nomcom', None)
@ -766,7 +770,7 @@ class MutableFeedbackForm(forms.ModelForm):
widget=forms.SelectMultiple(attrs={'class':'nominee_multi_select','size':'12'}), widget=forms.SelectMultiple(attrs={'class':'nominee_multi_select','size':'12'}),
help_text='Hold down "Control", or "Command" on a Mac, to select more than one.') help_text='Hold down "Control", or "Command" on a Mac, to select more than one.')
else: else:
self.fields['position'] = forms.ModelChoiceField(queryset=Position.objects.get_by_nomcom(self.nomcom).opened(), label="Position") self.fields['position'] = forms.ModelChoiceField(queryset=Position.objects.get_by_nomcom(self.nomcom).filter(is_open=True), label="Position")
self.fields['searched_email'] = SearchableEmailField(only_users=False,help_text="Try to find the candidate you are classifying with this field first. Only use the name and email fields below if this search does not find the candidate.",label="Candidate",required=False) self.fields['searched_email'] = SearchableEmailField(only_users=False,help_text="Try to find the candidate you are classifying with this field first. Only use the name and email fields below if this search does not find the candidate.",label="Candidate",required=False)
self.fields['candidate_name'] = forms.CharField(label="Candidate name",help_text="Only fill in this name field if the search doesn't find the person you are classifying",required=False) self.fields['candidate_name'] = forms.CharField(label="Candidate name",help_text="Only fill in this name field if the search doesn't find the person you are classifying",required=False)
self.fields['candidate_email'] = forms.EmailField(label="Candidate email",help_text="Only fill in this email field if the search doesn't find the person you are classifying",required=False) self.fields['candidate_email'] = forms.EmailField(label="Candidate email",help_text="Only fill in this email field if the search doesn't find the person you are classifying",required=False)
@ -830,7 +834,6 @@ class MutableFeedbackForm(forms.ModelForm):
feedback.positions.add(position) feedback.positions.add(position)
return feedback return feedback
FullFeedbackFormSet = forms.modelformset_factory( FullFeedbackFormSet = forms.modelformset_factory(
model=Feedback, model=Feedback,
extra=0, extra=0,

View file

@ -68,14 +68,6 @@ class PositionQuerySet(QuerySet):
def get_by_nomcom(self, nomcom): def get_by_nomcom(self, nomcom):
return self.filter(nomcom=nomcom) return self.filter(nomcom=nomcom)
def opened(self):
""" only opened positions """
return self.filter(is_open=True)
def closed(self):
""" only closed positions """
return self.filter(is_open=False)
class PositionManager(models.Manager, MixinManager): class PositionManager(models.Manager, MixinManager):
def get_queryset(self): def get_queryset(self):

View file

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-05-19 07:58
from __future__ import unicode_literals
from django.db import migrations, models
from django.db.models import F
def forward(apps, schema_editor):
Position = apps.get_model('nomcom','Position')
Position.objects.update(accepting_nominations=F('is_open'))
Position.objects.update(accepting_feedback=F('is_open'))
def reverse(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
('nomcom', '0012_auto_20170210_0205'),
]
operations = [
migrations.AddField(
model_name='position',
name='accepting_feedback',
field=models.BooleanField(default=False, verbose_name=b'Is accepting feedback'),
),
migrations.AddField(
model_name='position',
name='accepting_nominations',
field=models.BooleanField(default=False, verbose_name=b'Is accepting nominations'),
),
migrations.RunPython(forward,reverse)
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-05-22 08:19
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('nomcom', '0013_position_nomination_feedback_switches'),
]
operations = [
migrations.AlterField(
model_name='position',
name='is_open',
field=models.BooleanField(default=False, help_text=b'Set is_open when the nomcom is working on a position. Clear it when an appointment is confirmed.', verbose_name=b'Is open'),
),
]

View file

@ -163,7 +163,9 @@ class Position(models.Model):
name = models.CharField(verbose_name='Name', max_length=255, help_text='This short description will appear on the Nomination and Feedback pages. Be as descriptive as necessary. Past examples: "Transport AD", "IAB Member"') name = models.CharField(verbose_name='Name', max_length=255, help_text='This short description will appear on the Nomination and Feedback pages. Be as descriptive as necessary. Past examples: "Transport AD", "IAB Member"')
requirement = models.ForeignKey(DBTemplate, related_name='requirement', null=True, editable=False) requirement = models.ForeignKey(DBTemplate, related_name='requirement', null=True, editable=False)
questionnaire = models.ForeignKey(DBTemplate, related_name='questionnaire', null=True, editable=False) questionnaire = models.ForeignKey(DBTemplate, related_name='questionnaire', null=True, editable=False)
is_open = models.BooleanField(verbose_name='Is open', default=False) is_open = models.BooleanField(verbose_name='Is open', default=False, help_text="Set is_open when the nomcom is working on a position. Clear it when an appointment is confirmed.")
accepting_nominations = models.BooleanField(verbose_name='Is accepting nominations', default=False)
accepting_feedback = models.BooleanField(verbose_name='Is accepting feedback', default=False)
objects = PositionManager() objects = PositionManager()

View file

@ -42,6 +42,8 @@ class PositionResource(ModelResource):
"id": ALL, "id": ALL,
"name": ALL, "name": ALL,
"is_open": ALL, "is_open": ALL,
"accepting_nominations": ALL,
"accepting_feedback": ALL,
"nomcom": ALL_WITH_RELATIONS, "nomcom": ALL_WITH_RELATIONS,
"requirement": ALL_WITH_RELATIONS, "requirement": ALL_WITH_RELATIONS,
"questionnaire": ALL_WITH_RELATIONS, "questionnaire": ALL_WITH_RELATIONS,

View file

@ -130,7 +130,9 @@ def nomcom_test_data():
for name in POSITIONS: for name in POSITIONS:
position, created = Position.objects.get_or_create(nomcom=nomcom, position, created = Position.objects.get_or_create(nomcom=nomcom,
name=name, name=name,
is_open=True) is_open=True,
accepting_nominations=True,
accepting_feedback=True)
ChangeStateGroupEvent.objects.get_or_create(group=group, ChangeStateGroupEvent.objects.get_or_create(group=group,
type="changed_state", type="changed_state",

View file

@ -1771,3 +1771,93 @@ class MergePersonTests(TestCase):
self.assertEqual(self.nc.nominee_set.get(pk=self.nominee2.pk).feedback_set.count(),2) self.assertEqual(self.nc.nominee_set.get(pk=self.nominee2.pk).feedback_set.count(),2)
self.assertFalse(self.nc.nominee_set.filter(pk=self.nominee1.pk).exists()) self.assertFalse(self.nc.nominee_set.filter(pk=self.nominee1.pk).exists())
class AcceptingTests(TestCase):
def setUp(self):
build_test_public_keys_dir(self)
self.nc = NomComFactory(**nomcom_kwargs_for_year())
self.plain_person = PersonFactory.create()
self.member = self.nc.group.role_set.filter(name='member').first().person
def tearDown(self):
clean_test_public_keys_dir(self)
def test_public_accepting_nominations(self):
url = reverse('ietf.nomcom.views.public_nominate',kwargs={'year':self.nc.year()})
login_testing_unauthorized(self,self.plain_person.user.username,url)
response = self.client.get(url)
q=PyQuery(response.content)
self.assertEqual( len(q('#id_position option')) , 4 )
pos = self.nc.position_set.first()
pos.accepting_nominations=False
pos.save()
response = self.client.get(url)
q=PyQuery(response.content)
self.assertEqual( len(q('#id_position option')) , 3 )
def test_private_accepting_nominations(self):
url = reverse('ietf.nomcom.views.private_nominate',kwargs={'year':self.nc.year()})
login_testing_unauthorized(self,self.member.user.username,url)
response = self.client.get(url)
q=PyQuery(response.content)
self.assertEqual( len(q('#id_position option')) , 4 )
pos = self.nc.position_set.first()
pos.accepting_nominations=False
pos.save()
response = self.client.get(url)
q=PyQuery(response.content)
self.assertEqual( len(q('#id_position option')) , 4 )
def test_public_accepting_feedback(self):
url = reverse('ietf.nomcom.views.public_feedback',kwargs={'year':self.nc.year()})
login_testing_unauthorized(self,self.plain_person.user.username,url)
response = self.client.get(url)
q=PyQuery(response.content)
self.assertEqual( len(q('.badge')) , 3 )
pos = self.nc.position_set.first()
pos.accepting_feedback=False
pos.save()
response = self.client.get(url)
q=PyQuery(response.content)
self.assertEqual( len(q('.badge')) , 2 )
url += "?nominee=%d&position=%d" % (pos.nominee_set.first().pk, pos.pk)
response = self.client.get(url)
self.assertTrue('not currently accepting feedback' in unicontent(response))
test_data = {'comments': 'junk',
'position_name': pos.name,
'nominee_name': pos.nominee_set.first().email.person.name,
'nominee_email': pos.nominee_set.first().email.address,
'confirmation': False,
'nominator_email': self.plain_person.email().address,
'nominator_name': self.plain_person.plain_name(),
}
response = self.client.post(url, test_data)
self.assertTrue('not currently accepting feedback' in unicontent(response))
def test_private_accepting_feedback(self):
url = reverse('ietf.nomcom.views.private_feedback',kwargs={'year':self.nc.year()})
login_testing_unauthorized(self,self.member.user.username,url)
response = self.client.get(url)
q=PyQuery(response.content)
self.assertEqual( len(q('.badge')) , 3 )
pos = self.nc.position_set.first()
pos.accepting_feedback=False
pos.save()
response = self.client.get(url)
q=PyQuery(response.content)
self.assertEqual( len(q('.badge')) , 3 )

View file

@ -173,6 +173,7 @@ def private_index(request, year):
if selected_state == questionnaire_state: if selected_state == questionnaire_state:
nominee_positions = [np for np in nominee_positions if np.questionnaires] nominee_positions = [np for np in nominee_positions if np.questionnaires]
# TODO- just build this dict using open Positions: see #2254
stats = all_nominee_positions.values('position__name', 'position__id').annotate(total=Count('position')) stats = all_nominee_positions.values('position__name', 'position__id').annotate(total=Count('position'))
states = list(NomineePositionStateName.objects.values('slug', 'name')) + [{'slug': questionnaire_state, 'name': u'Questionnaire'}] states = list(NomineePositionStateName.objects.values('slug', 'name')) + [{'slug': questionnaire_state, 'name': u'Questionnaire'}]
positions = set([ n.position for n in all_nominee_positions.order_by('position__name') ]) positions = set([ n.position for n in all_nominee_positions.order_by('position__name') ])
@ -421,7 +422,10 @@ def feedback(request, year, public):
nominee = get_object_or_404(Nominee, id=selected_nominee) nominee = get_object_or_404(Nominee, id=selected_nominee)
position = get_object_or_404(Position, id=selected_position) position = get_object_or_404(Position, id=selected_position)
positions = Position.objects.get_by_nomcom(nomcom=nomcom).opened() if public:
positions = Position.objects.get_by_nomcom(nomcom=nomcom).filter(is_open=True,accepting_feedback=True)
else:
positions = Position.objects.get_by_nomcom(nomcom=nomcom).filter(is_open=True)
user_comments = Feedback.objects.filter(nomcom=nomcom, user_comments = Feedback.objects.filter(nomcom=nomcom,
type='comment', type='comment',
@ -445,6 +449,18 @@ def feedback(request, year, public):
'base_template': base_template 'base_template': base_template
}) })
if public and position and not (position.is_open and position.accepting_feedback):
messages.warning(request, "This Nomcom is not currently accepting feedback for "+position.name)
return render(request, 'nomcom/feedback.html', {
'form': None,
'nomcom': nomcom,
'year': year,
'selected': 'feedback',
'positions': positions,
'counts' : counts,
'base_template': base_template
})
if nominee and position and request.method == 'POST': if nominee and position and request.method == 'POST':
form = FeedbackForm(data=request.POST, form = FeedbackForm(data=request.POST,
nomcom=nomcom, user=request.user, nomcom=nomcom, user=request.user,

View file

@ -21,6 +21,16 @@
<div class="panel-body"> <div class="panel-body">
{% for position in group.list %} {% for position in group.list %}
<h4>{{ position.name }}</h4> <h4>{{ position.name }}</h4>
{% if group.grouper %}
<dl class="dl-horizontal">
<dt>Accepting</dt>
<dd>
{% if position.accepting_nominations %}Nominations{% endif %}
{% if position.accepting_nominations and position.accepting_feedback %}and{% endif %}
{% if position.accepting_feedback %}Feedback{% endif %}
</dd>
</dl>
{% endif %}
<dl class="dl-horizontal"> <dl class="dl-horizontal">
<dt>Templates</dt> <dt>Templates</dt>
<dd> <dd>