From 00e5762e20fa8fbab488d3bb8931c11ead4e9dcc Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Mon, 16 Nov 2015 21:46:29 +0000 Subject: [PATCH 01/37] Simplify the nomcom feedback comment form. Make that more obvious who receives mail when that form is used. Fixes #1849. - Legacy-Id: 10501 --- ietf/nomcom/forms.py | 63 +++++++---------------------- ietf/nomcom/tests.py | 11 +++-- ietf/nomcom/views.py | 11 +++-- ietf/templates/nomcom/feedback.html | 25 +++++++----- 4 files changed, 42 insertions(+), 68 deletions(-) diff --git a/ietf/nomcom/forms.py b/ietf/nomcom/forms.py index ef3dbdd8a..e86304b32 100644 --- a/ietf/nomcom/forms.py +++ b/ietf/nomcom/forms.py @@ -325,7 +325,7 @@ class MergeForm(BaseNomcomForm, forms.Form): class NominateForm(BaseNomcomForm, forms.ModelForm): comments = forms.CharField(label="Candidate's qualifications for the position", widget=forms.Textarea()) - confirmation = forms.BooleanField(label='Email comments back to me as confirmation', + confirmation = forms.BooleanField(label='Email comments back to me as confirmation.', help_text="If you want to get a confirmation mail containing your feedback in cleartext, please check the 'email comments back to me as confirmation'.", required=False) @@ -430,18 +430,11 @@ class NominateForm(BaseNomcomForm, forms.ModelForm): class FeedbackForm(BaseNomcomForm, forms.ModelForm): - position_name = forms.CharField(label='Position', - widget=forms.TextInput(attrs={'size': '40'})) - nominee_name = forms.CharField(label='Nominee name', - widget=forms.TextInput(attrs={'size': '40'})) - nominee_email = forms.CharField(label='Nominee email', - widget=forms.TextInput(attrs={'size': '40'})) - nominator_email = forms.CharField(label='Commenter email') + nominator_email = forms.CharField(label='Commenter email',required=False) - comments = forms.CharField(label='Comments on this nominee', + comments = forms.CharField(label='Comments', widget=forms.Textarea()) - confirmation = forms.BooleanField(label='Email comments back to me as confirmation', - help_text="If you want to get a confirmation mail containing your feedback in cleartext, please check the 'email comments back to me as confirmation'.", + confirmation = forms.BooleanField(label='Email comments back to me as confirmation (if selected, your comments will be emailed to you in cleartext when you press Save).', required=False) def __init__(self, *args, **kwargs): @@ -453,66 +446,38 @@ class FeedbackForm(BaseNomcomForm, forms.ModelForm): super(FeedbackForm, self).__init__(*args, **kwargs) - readonly_fields = ['position_name', - 'nominee_name', - 'nominee_email'] - - fieldset = ['position_name', - 'nominee_name', - 'nominee_email', - 'nominator_email', - 'comments'] + author = get_user_email(self.user) if self.public: - readonly_fields += ['nominator_email'] - fieldset.append('confirmation') + self.fields.pop('nominator_email') else: help_text = """(Nomcom Chair/Member: please fill this in. Use your own email address if the person making the comments wishes to be anonymous. The confirmation email will be sent to the address given here, and the address will also be captured as part of the registered nomination.)""" self.fields['nominator_email'].help_text = help_text - self.fields['nominator_email'].required = False + self.fields['confirmation'].label = 'Email these comments in cleartext to the provided commenter email address' + if author: + self.fields['nominator_email'].initial = author.address - author = get_user_email(self.user) - if author: - self.fields['nominator_email'].initial = author.address - - if self.position and self.nominee: - self.fields['position_name'].initial = self.position.name - self.fields['nominee_name'].initial = self.nominee.email.person.name - self.fields['nominee_email'].initial = self.nominee.email.address - else: - help_text = "Please pick a name on the nominees list" - self.fields['position_name'].initial = help_text - self.fields['nominee_name'].initial = help_text - self.fields['nominee_email'].initial = help_text - self.fields['comments'].initial = help_text - readonly_fields += ['comments'] - self.fields['confirmation'].widget.attrs['disabled'] = "disabled" - - for field in readonly_fields: - self.fields[field].widget.attrs['readonly'] = True - - self.fieldsets = [('Provide comments', fieldset)] def clean(self): if not NomineePosition.objects.accepted().filter(nominee=self.nominee, position=self.position): msg = "There isn't a accepted nomination for %s on the %s position" % (self.nominee, self.position) - self._errors["nominee_email"] = self.error_class([msg]) + self._errors["comments"] = self.error_class([msg]) return self.cleaned_data def save(self, commit=True): feedback = super(FeedbackForm, self).save(commit=False) confirmation = self.cleaned_data['confirmation'] comments = self.cleaned_data['comments'] - nominator_email = self.cleaned_data['nominator_email'] nomcom_template_path = '/nomcom/%s/' % self.nomcom.group.acronym author = None if self.public: author = get_user_email(self.user) else: + nominator_email = self.cleaned_data['nominator_email'] if nominator_email: emails = Email.objects.filter(address=nominator_email) author = emails and emails[0] or None @@ -541,11 +506,11 @@ class FeedbackForm(BaseNomcomForm, forms.ModelForm): class Meta: model = Feedback - fields = ('nominee_name', - 'nominee_email', + fields = ( 'nominator_email', + 'comments', 'confirmation', - 'comments') + ) class FeedbackEmailForm(BaseNomcomForm, forms.Form): diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py index a5babc108..e81546f04 100644 --- a/ietf/nomcom/tests.py +++ b/ietf/nomcom/tests.py @@ -657,7 +657,6 @@ class NomcomViewsTest(TestCase): def test_private_feedback(self): self.access_member_url(self.private_feedback_url) return self.feedback_view(public=False) - self.client.logout() def feedback_view(self, *args, **kwargs): public = kwargs.pop('public', True) @@ -688,11 +687,16 @@ class NomcomViewsTest(TestCase): response = self.client.get(feedback_url) self.assertEqual(response.status_code, 200) - self.assertContains(response, "feedbackform") + self.assertNotContains(response, "feedbackform") position = Position.objects.get(name=position_name) nominee = Nominee.objects.get(email__address=nominee_email) + feedback_url += "?nominee=%d&position=%d" % (nominee.id, position.id) + response = self.client.get(feedback_url) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "feedbackform") + comments = u'Test feedback view. Comments with accents äöåÄÖÅ éáíóú âêîôû ü àèìòù.' test_data = {'comments': comments, @@ -705,8 +709,6 @@ class NomcomViewsTest(TestCase): test_data['nominator_email'] = nominator_email test_data['nominator_name'] = nominator_email - feedback_url += "?nominee=%d&position=%d" % (nominee.id, position.id) - nominee_position = NomineePosition.objects.get(nominee=nominee, position=position) state = nominee_position.state @@ -722,6 +724,7 @@ class NomcomViewsTest(TestCase): response = self.client.post(feedback_url, test_data) self.assertEqual(response.status_code, 200) self.assertContains(response, "alert-success") + self.assertNotContains(response, "feedbackform") ## check objects feedback = Feedback.objects.filter(positions__in=[position], diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py index 3515a91cc..e4d3a3ff4 100644 --- a/ietf/nomcom/views.py +++ b/ietf/nomcom/views.py @@ -349,7 +349,6 @@ def private_feedback(request, year): def feedback(request, year, public): nomcom = get_nomcom_by_year(year) has_publickey = nomcom.public_key and True or False - submit_disabled = True nominee = None position = None selected_nominee = request.GET.get('nominee') @@ -357,7 +356,6 @@ def feedback(request, year, public): if selected_nominee and selected_position: nominee = get_object_or_404(Nominee, id=selected_nominee) position = get_object_or_404(Position, id=selected_position) - submit_disabled = False positions = Position.objects.get_by_nomcom(nomcom=nomcom).opened() @@ -384,11 +382,13 @@ def feedback(request, year, public): if form.is_valid(): form.save() message = ('success', 'Your feedback has been registered.') + form = None + else: + if nominee and position: form = FeedbackForm(nomcom=nomcom, user=request.user, public=public, position=position, nominee=nominee) - else: - form = FeedbackForm(nomcom=nomcom, user=request.user, public=public, - position=position, nominee=nominee) + else: + form = None return render(request, 'nomcom/feedback.html', { 'form': form, @@ -396,7 +396,6 @@ def feedback(request, year, public): 'nomcom': nomcom, 'year': year, 'positions': positions, - 'submit_disabled': submit_disabled, 'selected': 'feedback', 'base_template': base_template }) diff --git a/ietf/templates/nomcom/feedback.html b/ietf/templates/nomcom/feedback.html index b6e9bc19a..7334e9362 100644 --- a/ietf/templates/nomcom/feedback.html +++ b/ietf/templates/nomcom/feedback.html @@ -10,8 +10,7 @@ {% block nomcom_content %} {% origin %}

- First select a nominee from the list of nominees to provide input about that nominee. - This will fill in the non-editable fields in the form. + Select a nominee from the list of nominees to the right to obtain a new feedback form.

{% if message %} @@ -49,15 +48,23 @@
-

Provide feedback

+ {% if form %} +

Provide feedback + {% if form.position %} + about {{form.nominee.email.person.name}} ({{form.nominee.email.address}}) for the {{form.position.name}} position.

+ {% endif %} +

+

This feedback will only be available to NomCom {{year}}. + You may have the feedback mailed back to you by selecting the option below.

-
- {% csrf_token %} - {% bootstrap_form form %} - {% buttons %} - - {% endbuttons %} + + {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + + {% endbuttons %}
+ {% endif %}
{% endif %} From 21972591027f61fc39075f986379b919feea82c7 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Tue, 17 Nov 2015 22:50:35 +0000 Subject: [PATCH 02/37] Reorganized the tab navigation on the nomcom private pages. Made it more obvious when the chair is doing something that only the chair gets to see. Fixes #1788 and #1795. - Legacy-Id: 10505 --- ietf/nomcom/templatetags/nomcom_tags.py | 7 +-- ietf/nomcom/views.py | 49 ++++++++++++++----- .../templates/nomcom/nomcom_private_base.html | 25 ++++++++-- 3 files changed, 61 insertions(+), 20 deletions(-) diff --git a/ietf/nomcom/templatetags/nomcom_tags.py b/ietf/nomcom/templatetags/nomcom_tags.py index 85a023123..f33b7490d 100644 --- a/ietf/nomcom/templatetags/nomcom_tags.py +++ b/ietf/nomcom/templatetags/nomcom_tags.py @@ -7,7 +7,6 @@ from django.template.defaultfilters import linebreaksbr, force_escape from ietf.utils.pipe import pipe from ietf.utils.log import log -from ietf.ietfauth.utils import has_role from ietf.doc.templatetags.ietf_filters import wrap_text from ietf.person.models import Person @@ -19,13 +18,11 @@ register = template.Library() @register.filter -def is_chair(user, year): +def is_chair_or_advisor(user, year): if not user or not year: return False nomcom = get_nomcom_by_year(year=year) - if has_role(user, "Secretariat"): - return True - return nomcom.group.has_role(user, "chair") + return nomcom.group.has_role(user, ["chair","advisor"]) @register.filter diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py index e4d3a3ff4..f6554cb8c 100644 --- a/ietf/nomcom/views.py +++ b/ietf/nomcom/views.py @@ -249,7 +249,9 @@ def send_reminder_mail(request, year, type): 'selected': selected_tab, 'reminder_description': reminder_description, 'state_description': state_description, - 'message': message}, RequestContext(request)) + 'message': message, + 'is_chair_task' : True, + }, RequestContext(request)) @role_required("Nomcom Chair", "Nomcom Advisor") @@ -269,7 +271,9 @@ def private_merge(request, year): 'year': year, 'form': form, 'message': message, - 'selected': 'merge'}, RequestContext(request)) + 'selected': 'merge', + 'is_chair_task' : True, + }, RequestContext(request)) def requirements(request, year): @@ -414,7 +418,9 @@ def private_feedback_email(request, year): {'message': message, 'nomcom': nomcom, 'year': year, - 'selected': 'feedback_email'}, RequestContext(request)) + 'selected': 'feedback_email', + 'is_chair_task' : True, + }, RequestContext(request)) form = FeedbackEmailForm(nomcom=nomcom) @@ -448,7 +454,9 @@ def private_questionnaire(request, year): {'message': message, 'nomcom': nomcom, 'year': year, - 'selected': 'questionnaire'}, RequestContext(request)) + 'selected': 'questionnaire', + 'is_chair_task' : True, + }, RequestContext(request)) if request.method == 'POST': form = QuestionnaireForm(data=request.POST, @@ -649,7 +657,9 @@ def view_feedback_pending(request, year): 'type_dict': type_dict, 'extra_ids': extra_ids, 'types': FeedbackTypeName.objects.all().order_by('pk'), - 'nomcom': nomcom}, RequestContext(request)) + 'nomcom': nomcom, + 'is_chair_task' : True, + }, RequestContext(request)) @role_required("Nomcom") @@ -704,7 +714,9 @@ def edit_nominee(request, year, nominee_id): 'nominee': nominee, 'form': form, 'message': message, - 'nomcom': nomcom}, RequestContext(request)) + 'nomcom': nomcom, + 'is_chair_task' : True, + }, RequestContext(request)) @role_required("Nomcom Chair", "Nomcom Advisor") @@ -739,7 +751,9 @@ def edit_nomcom(request, year): 'nomcom': nomcom, 'message': message, 'year': year, - 'selected': 'edit_nomcom'}, RequestContext(request)) + 'selected': 'edit_nomcom', + 'is_chair_task' : True, + }, RequestContext(request)) @role_required("Nomcom Chair", "Nomcom Advisor") @@ -755,6 +769,7 @@ def delete_nomcom(request, year): 'year': year, 'selected': 'edit_nomcom', 'nomcom': nomcom, + 'is_chair_task' : True, }) @@ -769,7 +784,9 @@ def list_templates(request, year): 'positions': positions, 'year': year, 'selected': 'edit_templates', - 'nomcom': nomcom}, RequestContext(request)) + 'nomcom': nomcom, + 'is_chair_task' : True, + }, RequestContext(request)) @role_required("Nomcom Chair", "Nomcom Advisor") @@ -782,7 +799,9 @@ def edit_template(request, year, template_id): formclass=NomComTemplateForm, extra_context={'year': year, 'return_url': return_url, - 'nomcom': nomcom}) + 'nomcom': nomcom, + 'is_chair_task' : True, + }) @role_required("Nomcom Chair", "Nomcom Advisor") @@ -794,7 +813,9 @@ def list_positions(request, year): {'positions': positions, 'year': year, 'selected': 'edit_positions', - 'nomcom': nomcom}, RequestContext(request)) + 'nomcom': nomcom, + 'is_chair_task' : True, + }, RequestContext(request)) @role_required("Nomcom Chair", "Nomcom Advisor") @@ -811,7 +832,9 @@ def remove_position(request, year, position_id): return render_to_response('nomcom/remove_position.html', {'year': year, 'position': position, - 'nomcom': nomcom}, RequestContext(request)) + 'nomcom': nomcom, + 'is_chair_task' : True, + }, RequestContext(request)) @role_required("Nomcom Chair", "Nomcom Advisor") @@ -837,4 +860,6 @@ def edit_position(request, year, position_id=None): {'form': form, 'position': position, 'year': year, - 'nomcom': nomcom}, RequestContext(request)) + 'nomcom': nomcom, + 'is_chair_task' : True, + }, RequestContext(request)) diff --git a/ietf/templates/nomcom/nomcom_private_base.html b/ietf/templates/nomcom/nomcom_private_base.html index 46d2ba59d..cb9e89384 100644 --- a/ietf/templates/nomcom/nomcom_private_base.html +++ b/ietf/templates/nomcom/nomcom_private_base.html @@ -9,7 +9,7 @@ {% block content %} {% origin %} -

NomCom {{ year }} Private area

+

NomCom {{ year }} Private area {% if is_chair_task %}- Chair/Advisors only{% endif %}

From e81b47328258a24af82d9173b3676be9a6d745b2 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 25 Nov 2015 22:17:41 +0000 Subject: [PATCH 03/37] Expose views for concluded nomcoms. Close feedback and nomination. Initial work on factory-boy based testing. Partially addresses #1856 - Legacy-Id: 10520 --- ietf/group/factories.py | 10 ++ ietf/ietfauth/utils.py | 6 +- ietf/nomcom/factories.py | 125 ++++++++++++++++++ ietf/nomcom/models.py | 8 ++ ietf/nomcom/tests.py | 51 +++++++ ietf/nomcom/utils.py | 2 +- ietf/nomcom/views.py | 21 ++- ietf/person/factories.py | 56 ++++++++ ietf/templates/nomcom/feedback.html | 12 +- .../templates/nomcom/nomcom_private_base.html | 2 +- ietf/templates/nomcom/nomcom_public_base.html | 2 +- ietf/templates/nomcom/private_nominate.html | 2 +- ietf/templates/nomcom/public_nominate.html | 2 +- requirements.txt | 2 + 14 files changed, 283 insertions(+), 18 deletions(-) create mode 100644 ietf/group/factories.py create mode 100644 ietf/nomcom/factories.py create mode 100644 ietf/person/factories.py diff --git a/ietf/group/factories.py b/ietf/group/factories.py new file mode 100644 index 000000000..9cef50afd --- /dev/null +++ b/ietf/group/factories.py @@ -0,0 +1,10 @@ +import factory + +from ietf.group.models import Group + +class GroupFactory(factory.DjangoModelFactory): + class Meta: + model = Group + + name = factory.Faker('sentence',nb_words=6) + acronym = factory.Faker('word') diff --git a/ietf/ietfauth/utils.py b/ietf/ietfauth/utils.py index 2a1db3938..e28587aec 100644 --- a/ietf/ietfauth/utils.py +++ b/ietf/ietfauth/utils.py @@ -64,9 +64,9 @@ def has_role(user, role_names, *args, **kwargs): "RG Secretary": Q(person=person,name="secr", group__type="rg", group__state__in=["active","proposed"]), "AG Secretary": Q(person=person,name="secr", group__type="ag", group__state__in=["active"]), "Team Chair": Q(person=person,name="chair", group__type="team", group__state="active"), - "Nomcom Chair": Q(person=person, name="chair", group__type="nomcom", group__state="active", group__acronym__icontains=kwargs.get('year', '0000')), - "Nomcom Advisor": Q(person=person, name="advisor", group__type="nomcom", group__state="active", group__acronym__icontains=kwargs.get('year', '0000')), - "Nomcom": Q(person=person, group__type="nomcom", group__state="active", group__acronym__icontains=kwargs.get('year', '0000')), + "Nomcom Chair": Q(person=person, name="chair", group__type="nomcom", group__acronym__icontains=kwargs.get('year', '0000')), + "Nomcom Advisor": Q(person=person, name="advisor", group__type="nomcom", group__acronym__icontains=kwargs.get('year', '0000')), + "Nomcom": Q(person=person, group__type="nomcom", group__acronym__icontains=kwargs.get('year', '0000')), "Liaison Manager": Q(person=person,name="liaiman",group__type="sdo",group__state="active", ), "Authorized Individual": Q(person=person,name="auth",group__type="sdo",group__state="active", ), } diff --git a/ietf/nomcom/factories.py b/ietf/nomcom/factories.py new file mode 100644 index 000000000..eed265eff --- /dev/null +++ b/ietf/nomcom/factories.py @@ -0,0 +1,125 @@ +import factory +import random + +from ietf.nomcom.models import NomCom, Position, Nominee, NomineePosition +from ietf.group.factories import GroupFactory +from ietf.person.factories import PersonFactory + +import debug # pyflakes:ignore + +cert = '''-----BEGIN CERTIFICATE----- +MIIDHjCCAgagAwIBAgIJAKDCCjbQboJzMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV +BAMMCE5vbUNvbTE1MB4XDTE0MDQwNDIxMTQxNFoXDTE2MDQwMzIxMTQxNFowEzER +MA8GA1UEAwwITm9tQ29tMTUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQC2QXCsAitYSOgPYor77zQnEeHuVqlcuhpH1wpKB+N6WcScA5N3AnX9uZEFOt6M +cJ+MCiHECdqDlH6npQTJlpCpIVgAD4B6xzjRBRww8d3lClA/kKwsKzuX93RS0Uv3 +0hAD6q9wjqK/m6vR5Y1SsvJYV0y+Yu5j9xUEsojMH7O3NlXWAYOb6oH+f/X7PX27 +IhtiCwfICMmVWh/hKeXuFx6HSOcH3gZ6Tlk1llfDbE/ArpsZ6JmnLn73+64yqIoO +ZOc4JJUPrdsmbNwXoxQSQhrpwjN8NpSkQaJbHGB3G+OWvP4fpqcweFHxlEq1Hhef +uR9E6jc3qwxVQfwjbcq6N/4JAgMBAAGjdTBzMB0GA1UdDgQWBBTJow+TJynRWsTQ +LzoS861FGb/rxDAOBgNVHQ8BAf8EBAMCBLAwDwYDVR0TAQH/BAUwAwEB/zAcBgNV +HREEFTATgRFub21jb20xNUBpZXRmLm9yZzATBgNVHSUEDDAKBggrBgEFBQcDBDAN +BgkqhkiG9w0BAQsFAAOCAQEAJwLapB9u5N3iK6SCTqh+PVkigZeB2YMVBW8WA3Ut +iRPBj+jHWOpF5pzZHTOcNaAxDEG9lyIlcWqc93A24K/Gen11Tx0hO4FAPOG0+PP8 +4lx7F6xeeyUNR44pInrB93G2q0jl+3wjZH8uhBKlGji4UTMpDPpEl6uiyQCbkMMm +Vr7HZH5Dv/lsjGHHf8uJO7+mcMh+tqxLn3DzPrm61OfeWdkoVX2pTz0imRQ3Es+8 +I7zNMk+fNNaEEyPnEyHfuWq0uD/qKeP27NZIoINy6E3INQ5QaE2uc1nQULg5y7uJ +toX3j+FUe2UiUak3ACXdrOPSsFP0KRrFwuMnuHHXkGj/Uw== +-----END CERTIFICATE----- +''' + +key = '''-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC2QXCsAitYSOgP +Yor77zQnEeHuVqlcuhpH1wpKB+N6WcScA5N3AnX9uZEFOt6McJ+MCiHECdqDlH6n +pQTJlpCpIVgAD4B6xzjRBRww8d3lClA/kKwsKzuX93RS0Uv30hAD6q9wjqK/m6vR +5Y1SsvJYV0y+Yu5j9xUEsojMH7O3NlXWAYOb6oH+f/X7PX27IhtiCwfICMmVWh/h +KeXuFx6HSOcH3gZ6Tlk1llfDbE/ArpsZ6JmnLn73+64yqIoOZOc4JJUPrdsmbNwX +oxQSQhrpwjN8NpSkQaJbHGB3G+OWvP4fpqcweFHxlEq1HhefuR9E6jc3qwxVQfwj +bcq6N/4JAgMBAAECggEAb5SS4YwWc193S2v+QQ2KdVz6YEuINq/tRQw/TWGVACQT +PZzm3FaSXDsOsRAAjiSpWTgewgFyWVpBTGu4CZ73g8RZNvhGpWRwwW8KemCpg/8T +cEcnUYdKXdhuzAE9LETb7znwHM4Gj55DzCZopjfOLQ2Ne4XgAy2THaQcIjRKd6Bw +3mteJ2ityDj3iFN7cq9ntDzp+2BqLOi7AZmLntmUZxtkPCT6k5/dcKFYQW9Eb3bt +MON+BIYVzqhAijkP/cAWmbgZAP9EFng5PpE1lc/shl0W8eX4yvjNoMPRq3wphS4j +L16VncUeDep3vR0CECx7gnTfR0uCDEgKow50pzGQAQKBgQDaQWwK/o39zI3lCGzy +oSNJRNQJ/iZBkbbwpCCaka7VnBfd0ZH54VEWL3oMTkkWRSZtjsPAqT+ndwZitm0D +Kww9FUDMP7j/tMOwAUHYfjYFqFTn6ipkBuby9tbZtL7lgJO6Iu2Qk3afqADD0kcP +zRLxcYSLjrmp9NyUlNnpswR4CQKBgQDVxjwG/orCmiuyA1Bu4u1hdUD0w9CKnyjp +VTbkv8lxk5V3pYzms2Awb0X43W2OioYGBk5yw+9GCF//xCrfbGV7BLZnDTGShjkJ +8oTpLPGBsDSfaKVXE3Hko4LVLBMQIm0tDyuPD1Naia7ZknYn906skonEG8WgHUyp +c/BgkvzWAQKBgBdojuL6/FWtO8bFyZGYUMWJ+Uf9FzNPIpTatZh+aYcFj9W9pW9s +iBreCrQJLXOTBRUZC8u9G1Olw2yQ7k45rr1aazG83+WlCJv29o32s2qV7E1XYyaJ +SvniGZcN+K96w91h46Lu/fkPts1J309FinOU3kdtjmI5HfNdp6WWCrOpAoGBAMjc +TEaeIK8cwPWwG4E1A6pQy8mvu2Ckj4I+KSfh9FsdOpGDIdMas8SOqQZet7P5AFjk +0A0RgN8iu2DMZyQq62cdVG2bffqY1zs7fhrBueILOEaXwtMAWEFmSWYW1YqRbleq +K1luIvms6HdSIGcI/gk0XvG+zn/VR9ToNPHo6lwBAoGBAIrYGYPf+cjZ1V/tNqnL +IecEZb4Gkp1hVhOpNT4U+T2LROxrZtFxxsw2vuIRa5a5FtMbDq9Xyhkm0QppliBd +KQ38jTT0EaD2+vstTqL8vxupo25RQWV1XsmLL4pLbKnm2HnnwB3vEtsiokWKW0q0 +Tdb0MiLc+r/zvx8oXtgDjDUa +-----END PRIVATE KEY----- +''' + +def nomcom_kwargs_for_year(year=None, *args, **kwargs): + if not year: + year = random.randint(1980,2100) + if 'group__state_id' not in kwargs: + kwargs['group__state_id']='active' + if 'group__acronym' not in kwargs: + kwargs['group__acronym'] = 'nomcom%d'%year + if 'group__name' not in kwargs: + kwargs['group__name'] = 'TEST VERSION of IAB/IESG Nominating Committee %d/%d'%(year,year+1) + return kwargs + + +class NomComFactory(factory.DjangoModelFactory): + class Meta: + model = NomCom + + group = factory.SubFactory(GroupFactory,type_id='nomcom') + + public_key = factory.django.FileField(data=cert) + + @factory.post_generation + def populate_positions(self, create, extracted, **kwargs): + ''' + Create a set of nominees and positions unless NomcomFactory is called + with populate_positions=False + ''' + if extracted is None: + extracted = True + if create and extracted: + nominees = [Nominee.objects.create(nomcom=self, email=PersonFactory().email_set.first()) for i in range(2)] + positions = [PositionFactory(nomcom=self) for i in range(3)] + + def npc(x,y): + return NomineePosition.objects.create(position=x, + nominee=y, + state_id='accepted') + # This gives us positions with 0, 1 and 2 nominees, and + # one person who's been nomminated for more than one position + npc(positions[0],nominees[0]) + npc(positions[1],nominees[0]) + npc(positions[1],nominees[1]) + + @factory.post_generation + def populate_personnel(self, create, extracted, **kwargs): + ''' + Create a default set of role holders, unless the factory is called + with populate_personnel=False + ''' + if extracted is None: + extracted = True + if create and extracted: + #roles= ['chair', 'advisor'] + ['member']*10 + roles = ['chair', 'advisor', 'member'] + for role in roles: + p = PersonFactory() + self.group.role_set.create(name_id=role,person=p,email=p.email_set.first()) + +class PositionFactory(factory.DjangoModelFactory): + class Meta: + model = Position + + name = factory.Faker('sentence',nb_words=10) + description = factory.Faker('paragraph',nb_sentences=4) + is_open = True + diff --git a/ietf/nomcom/models.py b/ietf/nomcom/models.py index 6b466dd98..36c295086 100644 --- a/ietf/nomcom/models.py +++ b/ietf/nomcom/models.py @@ -66,6 +66,14 @@ class NomCom(models.Model): if created: initialize_templates_for_group(self) + def year(self): + year = getattr(self,'_cached_year',None) + if year is None: + if self.group and self.group.acronym.startswith('nomcom'): + year = int(self.group.acronym[6:]) + self._cached_year = year + return year + def delete_nomcom(sender, **kwargs): nomcom = kwargs.get('instance', None) diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py index e81546f04..0520151aa 100644 --- a/ietf/nomcom/tests.py +++ b/ietf/nomcom/tests.py @@ -30,6 +30,9 @@ from ietf.nomcom.forms import EditMembersForm, EditMembersFormPreview from ietf.nomcom.utils import get_nomcom_by_year, get_or_create_nominee from ietf.nomcom.management.commands.send_reminders import Command, is_time_to_send +from ietf.nomcom.factories import NomComFactory, nomcom_kwargs_for_year +from ietf.person.factories import PersonFactory + client_test_cert_files = None def get_cert_files(): @@ -920,3 +923,51 @@ class ReminderTest(TestCase): self.assertEqual(len(outbox), messages_before + 1) self.assertTrue('nominee1@' in outbox[-1]['To']) +class InactiveNomcomTests(TestCase): + + def setUp(self): + self.nc = NomComFactory.create(**nomcom_kwargs_for_year(group__state_id='conclude')) + self.plain_person = PersonFactory.create() + + def test_feedback_closed(self): + for view in ['nomcom_public_feedback', 'nomcom_private_feedback']: + url = reverse(view, kwargs={'year': self.nc.year()}) + who = self.plain_person if 'public' in view else self.nc.group.role_set.filter(name='member').first().person + login_testing_unauthorized(self, who.user.username, url) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + q = PyQuery(response.content) + self.assertTrue( '(Concluded)' in q('h1').text()) + self.assertTrue( 'closed' in q('#instructions').text()) + self.assertTrue( q('#nominees a') ) + self.assertFalse( q('#nominees a[href]') ) + + url += "?nominee=%d&position=%d" % (self.nc.nominee_set.first().id, self.nc.nominee_set.first().nomineeposition_set.first().position.id) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + q = PyQuery(response.content) + self.assertFalse( q('#feedbackform')) + + empty_outbox() + fb_before = self.nc.feedback_set.count() + test_data = {'comments': u'Test feedback view. Comments with accents äöåÄÖÅ éáíóú âêîôû ü àèìòù.', + 'nominator_email': self.plain_person.email_set.first().address, + 'confirmation': True} + response = self.client.post(url, test_data) + self.assertEqual(response.status_code, 200) + q = PyQuery(response.content) + self.assertTrue( 'closed' in q('#instructions').text()) + self.assertEqual( len(outbox), 0 ) + self.assertEqual( fb_before, self.nc.feedback_set.count() ) + + def test_nominations_closed(self): + for view in ['nomcom_public_nominate', 'nomcom_private_nominate']: + url = reverse(view, kwargs={'year': self.nc.year() }) + who = self.plain_person if 'public' in view else self.nc.group.role_set.filter(name='member').first().person + login_testing_unauthorized(self, who.user.username, url) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + q = PyQuery(response.content) + self.assertTrue( '(Concluded)' in q('h1').text()) + self.assertTrue( 'closed' in q('.alert-warning').text()) + diff --git a/ietf/nomcom/utils.py b/ietf/nomcom/utils.py index 084cf7db7..55cdfa4fb 100644 --- a/ietf/nomcom/utils.py +++ b/ietf/nomcom/utils.py @@ -53,7 +53,7 @@ def get_nomcom_by_year(year): from ietf.nomcom.models import NomCom return get_object_or_404(NomCom, group__acronym__icontains=year, - group__state__slug='active') + ) def get_year_by_nomcom(nomcom): diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py index f6554cb8c..bafa77d55 100644 --- a/ietf/nomcom/views.py +++ b/ietf/nomcom/views.py @@ -322,6 +322,14 @@ def nominate(request, year, public): 'year': year, 'selected': 'nominate'}, RequestContext(request)) + if nomcom.group.state_id == 'conclude': + message = ('warning', "Nominations to this Nomcom are closed.") + return render_to_response(template, + {'message': message, + 'nomcom': nomcom, + 'year': year, + 'selected': 'nominate'}, RequestContext(request)) + message = None if request.method == 'POST': form = NominateForm(data=request.POST, nomcom=nomcom, user=request.user, public=public) @@ -355,11 +363,12 @@ def feedback(request, year, public): has_publickey = nomcom.public_key and True or False nominee = None position = None - selected_nominee = request.GET.get('nominee') - selected_position = request.GET.get('position') - if selected_nominee and selected_position: - nominee = get_object_or_404(Nominee, id=selected_nominee) - position = get_object_or_404(Position, id=selected_position) + if nomcom.group.state_id != 'conclude': + selected_nominee = request.GET.get('nominee') + selected_position = request.GET.get('position') + if selected_nominee and selected_position: + nominee = get_object_or_404(Nominee, id=selected_nominee) + position = get_object_or_404(Position, id=selected_position) positions = Position.objects.get_by_nomcom(nomcom=nomcom).opened() @@ -379,7 +388,7 @@ def feedback(request, year, public): }) message = None - if request.method == 'POST': + if nominee and position and request.method == 'POST': form = FeedbackForm(data=request.POST, nomcom=nomcom, user=request.user, public=public, position=position, nominee=nominee) diff --git a/ietf/person/factories.py b/ietf/person/factories.py new file mode 100644 index 000000000..79db6fcff --- /dev/null +++ b/ietf/person/factories.py @@ -0,0 +1,56 @@ +import factory +import faker + +from unidecode import unidecode + +from django.contrib.auth.models import User +from ietf.person.models import Person, Alias, Email + +fake = faker.Factory.create() + +class UserFactory(factory.DjangoModelFactory): + class Meta: + model = User + django_get_or_create = ('username',) + + first_name = factory.Faker('first_name') + last_name = factory.Faker('last_name') + email = factory.LazyAttribute(lambda u: '%s.%s@%s'%(u.first_name,u.last_name,fake.domain_name())) + username = factory.LazyAttribute(lambda u: u.email) + + @factory.post_generation + def set_password(self, create, extracted, **kwargs): + self.set_password( '%s+password' % self.username ) + +class PersonFactory(factory.DjangoModelFactory): + class Meta: + model = Person + + user = factory.SubFactory(UserFactory) + name = factory.LazyAttribute(lambda p: '%s %s'%(p.user.first_name,p.user.last_name)) + ascii = factory.LazyAttribute(lambda p: unidecode(p.name)) + + @factory.post_generation + def default_aliases(self, create, extracted, **kwargs): + make_alias = getattr(AliasFactory, 'create' if create else 'build') + make_alias(person=self,name=self.name) + make_alias(person=self,name=self.ascii) + + @factory.post_generation + def default_emails(self, create, extracted, **kwargs): + make_email = getattr(EmailFactory, 'create' if create else 'build') + make_email(person=self,address=self.user.email) + +class AliasFactory(factory.DjangoModelFactory): + class Meta: + model = Alias + django_get_or_create = ('name',) + + name = factory.Faker('name') + +class EmailFactory(factory.DjangoModelFactory): + class Meta: + model = Email + django_get_or_create = ('address',) + + address = '%s.%s@%s' % (factory.Faker('first_name'),factory.Faker('last_name'),factory.Faker('domain_name')) diff --git a/ietf/templates/nomcom/feedback.html b/ietf/templates/nomcom/feedback.html index 7334e9362..1a5e70310 100644 --- a/ietf/templates/nomcom/feedback.html +++ b/ietf/templates/nomcom/feedback.html @@ -9,8 +9,12 @@ {% block nomcom_content %} {% origin %} -

- Select a nominee from the list of nominees to the right to obtain a new feedback form. +

+ {% if nomcom.group.state_id == 'conclude' %} + Feedback to this nomcom is closed. + {% else %} + Select a nominee from the list of nominees to the right to obtain a new feedback form. + {% endif %}

{% if message %} @@ -21,7 +25,7 @@ {% if nomcom|has_publickey %}
-
+

Nominees

{% for p in positions %} @@ -29,7 +33,7 @@

{{ p.name }}

{% for np in p.nomineeposition_set.accepted.not_duplicated %} - + {{ np.nominee }} {% add_num_nominations user np.position np.nominee %} diff --git a/ietf/templates/nomcom/nomcom_private_base.html b/ietf/templates/nomcom/nomcom_private_base.html index cb9e89384..d597ce0f1 100644 --- a/ietf/templates/nomcom/nomcom_private_base.html +++ b/ietf/templates/nomcom/nomcom_private_base.html @@ -9,7 +9,7 @@ {% block content %} {% origin %} -

NomCom {{ year }} Private area {% if is_chair_task %}- Chair/Advisors only{% endif %}

+

NomCom {{ year }} {% if nomcom.group.state_id == 'conclude' %}(Concluded){% endif %} Private area {% if is_chair_task %}- Chair/Advisors only{% endif %}

From 2a63f91d31d04727074f8a0d9025bc89d1437690 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 2 Dec 2015 04:44:42 +0000 Subject: [PATCH 10/37] Simplify pass of counts - Legacy-Id: 10527 --- ietf/nomcom/templatetags/nomcom_tags.py | 7 ++----- ietf/nomcom/views.py | 5 +---- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/ietf/nomcom/templatetags/nomcom_tags.py b/ietf/nomcom/templatetags/nomcom_tags.py index e3ba1dbfa..dfbfd10c5 100644 --- a/ietf/nomcom/templatetags/nomcom_tags.py +++ b/ietf/nomcom/templatetags/nomcom_tags.py @@ -10,8 +10,7 @@ from ietf.utils.log import log from ietf.doc.templatetags.ietf_filters import wrap_text from ietf.person.models import Person -from ietf.nomcom.models import Feedback -from ietf.nomcom.utils import get_nomcom_by_year, get_user_email, retrieve_nomcom_private_key +from ietf.nomcom.utils import get_nomcom_by_year, retrieve_nomcom_private_key import debug # pyflakes:ignore @@ -34,9 +33,7 @@ def has_publickey(nomcom): @register.simple_tag def add_num_nominations(counts, position, nominee): - count = 0 - if position.id in counts and nominee.id in counts[position.id]: - count = counts[position.id][nominee.id] + count = counts.get((position.id,nominee.id),0) if count: return '%s ' % (count , nominee.email.address, position, count) else: diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py index b556020ab..eac2cf1fa 100644 --- a/ietf/nomcom/views.py +++ b/ietf/nomcom/views.py @@ -392,10 +392,7 @@ def feedback(request, year, public): user_comments = Feedback.objects.filter(nomcom=nomcom, type='comment', author__in=request.user.person.email_set.filter(active='True')) - counter = Counter(user_comments.values_list('positions','nominees')) - counts = dict() - for pos,nom in counter: - counts.setdefault(pos,dict())[nom] = counter[(pos,nom)] + counts = Counter(user_comments.values_list('positions','nominees')) if public: base_template = "nomcom/nomcom_public_base.html" From 5de9343359c46b9c8bbbc623b36e9407575f5b9c Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 2 Dec 2015 15:45:40 +0000 Subject: [PATCH 11/37] Removes the generation of the count badge from a templatetag into the template - Legacy-Id: 10531 --- ietf/nomcom/templatetags/nomcom_tags.py | 11 +++-------- ietf/nomcom/views.py | 6 ++++-- ietf/templates/nomcom/feedback.html | 13 +++++++++---- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/ietf/nomcom/templatetags/nomcom_tags.py b/ietf/nomcom/templatetags/nomcom_tags.py index dfbfd10c5..0af233808 100644 --- a/ietf/nomcom/templatetags/nomcom_tags.py +++ b/ietf/nomcom/templatetags/nomcom_tags.py @@ -30,14 +30,9 @@ def is_chair_or_advisor(user, year): def has_publickey(nomcom): return nomcom and nomcom.public_key and True or False - -@register.simple_tag -def add_num_nominations(counts, position, nominee): - count = counts.get((position.id,nominee.id),0) - if count: - return '%s ' % (count , nominee.email.address, position, count) - else: - return 'no feedback ' % (nominee.email.address, position) +@register.filter +def lookup(container,key): + return container and container.get(key,None) @register.filter def formatted_email(address): diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py index eac2cf1fa..c0af9349d 100644 --- a/ietf/nomcom/views.py +++ b/ietf/nomcom/views.py @@ -392,8 +392,10 @@ def feedback(request, year, public): user_comments = Feedback.objects.filter(nomcom=nomcom, type='comment', author__in=request.user.person.email_set.filter(active='True')) - counts = Counter(user_comments.values_list('positions','nominees')) - + counter = Counter(user_comments.values_list('positions','nominees')) + counts = dict() + for pos,nom in counter: + counts.setdefault(pos,dict())[nom] = counter[(pos,nom)] if public: base_template = "nomcom/nomcom_public_base.html" else: diff --git a/ietf/templates/nomcom/feedback.html b/ietf/templates/nomcom/feedback.html index d0dcd8cd9..4ca92057c 100644 --- a/ietf/templates/nomcom/feedback.html +++ b/ietf/templates/nomcom/feedback.html @@ -44,7 +44,12 @@ {% for np in p.nomineeposition_set.accepted.not_duplicated %} {{ np.nominee.name }} - {% add_num_nominations counts np.position np.nominee %} + {% with count=counts|lookup:np.position.id|lookup:np.nominee.id %} + + {{ count | default:"no feedback" }} +   + {% endwith %} {% endfor %}
@@ -54,9 +59,9 @@

An number after a name indicates that you have given comments on this nominee - earlier. If you position the mouse pointer over - it, you should see how many comments - exist from you for this nominee. + earlier. Position the mouse pointer over + the badge, for more information about this + nominee.

From d65987b9353b1f9a65cdce4caa925eeec2b4b610 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 2 Dec 2015 17:01:10 +0000 Subject: [PATCH 12/37] Improve the questionnaire templates, reminding the nominee that receiving the questionnaire does not imply they have accepted the nomination. Fixes #1807. - Legacy-Id: 10534 --- ...improve_default_questionnaire_templates.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 ietf/nomcom/migrations/0006_improve_default_questionnaire_templates.py diff --git a/ietf/nomcom/migrations/0006_improve_default_questionnaire_templates.py b/ietf/nomcom/migrations/0006_improve_default_questionnaire_templates.py new file mode 100644 index 000000000..f244ba969 --- /dev/null +++ b/ietf/nomcom/migrations/0006_improve_default_questionnaire_templates.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +def set_new_template_content(apps, schema_editor): + DBTemplate = apps.get_model('dbtemplate','DBTemplate') + + h = DBTemplate.objects.get(path='/nomcom/defaults/position/header_questionnaire.txt') + h.content = """Hi $nominee, this is the questionnaire for the position $position. +Please follow the directions in the questionnaire closely - you may see +that some changes have been made from previous years, so please take note. + +We look forward to reading your questionnaire response! If you have any +administrative questions, please send mail to nomcom-chair@ietf.org. + +You may have received this questionnaire before accepting the nomination. A +separate message, sent at the time of nomination, provides instructions for +indicating whether you accept or decline. If you have not completed those +steps, please do so as soon as possible, or contact the nomcom chair. + +Thank you! + + +""" + h.save() + + h = DBTemplate.objects.get(path='/nomcom/defaults/position/questionnaire.txt') + h.content = """NomCom Chair: Replace this content with the appropriate questionnaire for the position $position. +""" + h.save() + +def revert_to_old_template_content(apps, schema_editor): + DBTemplate = apps.get_model('dbtemplate','DBTemplate') + + h = DBTemplate.objects.get(path='/nomcom/defaults/position/header_questionnaire.txt') + h.content = """Hi $nominee, this is the questionnaire for the position $position. +Please follow the directions in the questionnaire closely - you may see +that some changes have been made from previous years, so please take note. + +We look forward to reading your questionnaire response! If you have any +administrative questions, please send mail to nomcom-chair@ietf.org. + +Thank you! + + +""" + h.save() + + h = DBTemplate.objects.get(path='/nomcom/defaults/position/questionnaire.txt') + h.content = """Enter here the questionnaire for the position $position: + +Questionnaire + +""" + h.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('nomcom', '0005_remove_position_incumbent'), + ] + + operations = [ + migrations.RunPython(set_new_template_content,revert_to_old_template_content) + ] From f566a83d1da69077e8fcd05b915648986f66dd57 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 2 Dec 2015 23:25:42 +0000 Subject: [PATCH 13/37] Captured when nomcom members reviewed feedback for a given nominee last. Added badges when new feedback is available. Improved layout of feedback index page. Fixes #1850. - Legacy-Id: 10535 --- ietf/nomcom/factories.py | 16 ++++- ...improve_default_questionnaire_templates.py | 2 +- .../migrations/0007_feedbacklastseen.py | 27 +++++++ ietf/nomcom/models.py | 7 +- ietf/nomcom/resources.py | 14 ++++ ietf/nomcom/tests.py | 71 ++++++++++++++++++- ietf/nomcom/views.py | 23 +++++- ietf/templates/nomcom/view_feedback.html | 14 ++-- .../nomcom/view_feedback_nominee.html | 2 +- 9 files changed, 162 insertions(+), 14 deletions(-) create mode 100644 ietf/nomcom/migrations/0007_feedbacklastseen.py diff --git a/ietf/nomcom/factories.py b/ietf/nomcom/factories.py index 0fd62609c..02791b872 100644 --- a/ietf/nomcom/factories.py +++ b/ietf/nomcom/factories.py @@ -1,7 +1,7 @@ import factory import random -from ietf.nomcom.models import NomCom, Position, Nominee, NomineePosition +from ietf.nomcom.models import NomCom, Position, Feedback, Nominee, NomineePosition from ietf.group.factories import GroupFactory from ietf.person.factories import PersonFactory @@ -128,3 +128,17 @@ class PositionFactory(factory.DjangoModelFactory): description = factory.Faker('paragraph',nb_sentences=4) is_open = True +class NomineeFactory(factory.DjangoModelFactory): + class Meta: + model = Nominee + + nomcom = factory.SubFactory(NomComFactory) + +class FeedbackFactory(factory.DjangoModelFactory): + class Meta: + model = Feedback + + nomcom = factory.SubFactory(NomComFactory) + subject = factory.Faker('sentence') + comments = factory.Faker('paragraph') + type_id = 'comment' diff --git a/ietf/nomcom/migrations/0006_improve_default_questionnaire_templates.py b/ietf/nomcom/migrations/0006_improve_default_questionnaire_templates.py index f244ba969..60b1622dd 100644 --- a/ietf/nomcom/migrations/0006_improve_default_questionnaire_templates.py +++ b/ietf/nomcom/migrations/0006_improve_default_questionnaire_templates.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations def set_new_template_content(apps, schema_editor): diff --git a/ietf/nomcom/migrations/0007_feedbacklastseen.py b/ietf/nomcom/migrations/0007_feedbacklastseen.py new file mode 100644 index 000000000..14f635a96 --- /dev/null +++ b/ietf/nomcom/migrations/0007_feedbacklastseen.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('person', '0004_auto_20150308_0440'), + ('nomcom', '0006_improve_default_questionnaire_templates'), + ] + + operations = [ + migrations.CreateModel( + name='FeedbackLastSeen', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('time', models.DateTimeField(auto_now=True)), + ('nominee', models.ForeignKey(to='nomcom.Nominee')), + ('reviewer', models.ForeignKey(to='person.Person')), + ], + options={ + }, + bases=(models.Model,), + ), + ] diff --git a/ietf/nomcom/models.py b/ietf/nomcom/models.py index 6b21d6faa..97247d860 100644 --- a/ietf/nomcom/models.py +++ b/ietf/nomcom/models.py @@ -9,7 +9,7 @@ from django.contrib.auth.models import User from django.template.loader import render_to_string from ietf.nomcom.fields import EncryptedTextField -from ietf.person.models import Email +from ietf.person.models import Person,Email from ietf.group.models import Group from ietf.name.models import NomineePositionStateName, FeedbackTypeName from ietf.dbtemplate.models import DBTemplate @@ -224,4 +224,7 @@ class Feedback(models.Model): class Meta: ordering = ['time'] - +class FeedbackLastSeen(models.Model): + reviewer = models.ForeignKey(Person) + nominee = models.ForeignKey(Nominee) + time = models.DateTimeField(auto_now=True) diff --git a/ietf/nomcom/resources.py b/ietf/nomcom/resources.py index 0e21f32e0..19583bb12 100644 --- a/ietf/nomcom/resources.py +++ b/ietf/nomcom/resources.py @@ -145,3 +145,17 @@ class NominationResource(ModelResource): } api.nomcom.register(NominationResource()) +from ietf.person.resources import PersonResource +class FeedbackLastSeenResource(ModelResource): + reviewer = ToOneField(PersonResource, 'reviewer') + nominee = ToOneField(NomineeResource, 'nominee') + class Meta: + queryset = FeedbackLastSeen.objects.all() + serializer = api.Serializer() + filtering = { + "id": ALL, + "time": ALL, + "reviewer": ALL_WITH_RELATIONS, + "nominee": ALL_WITH_RELATIONS, + } +api.nomcom.register(FeedbackLastSeenResource()) diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py index 4866a29ad..9de16385c 100644 --- a/ietf/nomcom/tests.py +++ b/ietf/nomcom/tests.py @@ -25,12 +25,13 @@ from ietf.nomcom.test_data import nomcom_test_data, generate_cert, check_comment MEMBER_USER, SECRETARIAT_USER, EMAIL_DOMAIN, NOMCOM_YEAR from ietf.nomcom.models import NomineePosition, Position, Nominee, \ NomineePositionStateName, Feedback, FeedbackTypeName, \ - Nomination + Nomination, FeedbackLastSeen from ietf.nomcom.forms import EditMembersForm, EditMembersFormPreview from ietf.nomcom.utils import get_nomcom_by_year, get_or_create_nominee, get_hash_nominee_position from ietf.nomcom.management.commands.send_reminders import Command, is_time_to_send -from ietf.nomcom.factories import NomComFactory, nomcom_kwargs_for_year, provide_private_key_to_test_client +from ietf.nomcom.factories import NomComFactory, FeedbackFactory, \ + nomcom_kwargs_for_year, provide_private_key_to_test_client from ietf.person.factories import PersonFactory from ietf.dbtemplate.factories import DBTemplateFactory @@ -1114,3 +1115,69 @@ class InactiveNomcomTests(TestCase): q = PyQuery(response.content) self.assertFalse( q('#templateform') ) +class FeedbackLastSeenTests(TestCase): + + def setUp(self): + self.nc = NomComFactory.create(**nomcom_kwargs_for_year(group__state_id='conclude')) + self.author = PersonFactory.create().email_set.first().address + self.member = self.nc.group.role_set.filter(name='member').first().person + self.nominee = self.nc.nominee_set.first() + self.position = self.nc.position_set.first() + for type_id in ['comment','nomina','questio']: + f = FeedbackFactory.create(author=self.author,nomcom=self.nc,type_id=type_id) + f.positions.add(self.position) + f.nominees.add(self.nominee) + now = datetime.datetime.now() + self.hour_ago = now - datetime.timedelta(hours=1) + self.half_hour_ago = now - datetime.timedelta(minutes=30) + self.second_from_now = now + datetime.timedelta(seconds=1) + + def test_feedback_index_badges(self): + url = reverse('nomcom_view_feedback',kwargs={'year':self.nc.year()}) + login_testing_unauthorized(self, self.member.user.username, url) + provide_private_key_to_test_client(self) + response = self.client.get(url) + self.assertEqual(response.status_code,200) + q = PyQuery(response.content) + self.assertEqual( len(q('.label-success')), 3 ) + + f = self.nc.feedback_set.first() + f.time = self.hour_ago + f.save() + FeedbackLastSeen.objects.create(reviewer=self.member,nominee=self.nominee) + FeedbackLastSeen.objects.update(time=self.half_hour_ago) + response = self.client.get(url) + self.assertEqual(response.status_code,200) + q = PyQuery(response.content) + self.assertEqual( len(q('.label-success')), 2 ) + + FeedbackLastSeen.objects.update(time=self.second_from_now) + response = self.client.get(url) + self.assertEqual(response.status_code,200) + q = PyQuery(response.content) + self.assertEqual( len(q('.label-success')), 0 ) + + def test_feedback_nominee_badges(self): + url = reverse('nomcom_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.get(url) + self.assertEqual(response.status_code,200) + q = PyQuery(response.content) + self.assertEqual( len(q('.label-success')), 3 ) + + f = self.nc.feedback_set.first() + f.time = self.hour_ago + f.save() + FeedbackLastSeen.objects.create(reviewer=self.member,nominee=self.nominee) + FeedbackLastSeen.objects.update(time=self.half_hour_ago) + response = self.client.get(url) + self.assertEqual(response.status_code,200) + q = PyQuery(response.content) + self.assertEqual( len(q('.label-success')), 2 ) + + FeedbackLastSeen.objects.update(time=self.second_from_now) + response = self.client.get(url) + self.assertEqual(response.status_code,200) + q = PyQuery(response.content) + self.assertEqual( len(q('.label-success')), 0 ) diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py index c0af9349d..3df144ba1 100644 --- a/ietf/nomcom/views.py +++ b/ietf/nomcom/views.py @@ -26,7 +26,7 @@ from ietf.nomcom.forms import (NominateForm, FeedbackForm, QuestionnaireForm, PrivateKeyForm, EditNomcomForm, EditNomineeForm, PendingFeedbackForm, ReminderDatesForm, FullFeedbackFormSet, FeedbackEmailForm) -from ietf.nomcom.models import Position, NomineePosition, Nominee, Feedback, NomCom, ReminderDates +from ietf.nomcom.models import Position, NomineePosition, Nominee, Feedback, NomCom, ReminderDates, FeedbackLastSeen from ietf.nomcom.utils import (get_nomcom_by_year, store_nomcom_private_key, get_hash_nominee_position, send_reminder_to_nominees, HOME_TEMPLATE, NOMINEE_ACCEPT_REMINDER_TEMPLATE,NOMINEE_QUESTIONNAIRE_REMINDER_TEMPLATE) @@ -579,7 +579,18 @@ def view_feedback(request, year): independent_feedback_types.append(ft) nominees_feedback = {} for nominee in nominees: - nominee_feedback = [(ft.name, nominee.feedback_set.by_type(ft.slug).count()) for ft in feedback_types] + last_seen = FeedbackLastSeen.objects.filter(reviewer=request.user.person,nominee=nominee).first() + nominee_feedback = [] + for ft in feedback_types: + qs = nominee.feedback_set.by_type(ft.slug) + count = qs.count() + if not count: + newflag = False + elif not last_seen: + newflag = True + else: + newflag = qs.filter(time__gt=last_seen.time).exists() + nominee_feedback.append( (ft.name,count,newflag) ) nominees_feedback.update({nominee: nominee_feedback}) independent_feedback = [ft.feedback_set.get_by_nomcom(nomcom).count() for ft in independent_feedback_types] @@ -736,11 +747,19 @@ def view_feedback_nominee(request, year, nominee_id): nominee = get_object_or_404(Nominee, id=nominee_id) feedback_types = FeedbackTypeName.objects.filter(slug__in=settings.NOMINEE_FEEDBACK_TYPES) + 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) + if last_seen: + last_seen.save() + else: + FeedbackLastSeen.objects.create(reviewer=request.user.person,nominee=nominee) + return render_to_response('nomcom/view_feedback_nominee.html', {'year': year, 'selected': 'view_feedback', 'nominee': nominee, 'feedback_types': feedback_types, + 'last_seen_time' : last_seen_time, 'nomcom': nomcom}, RequestContext(request)) diff --git a/ietf/templates/nomcom/view_feedback.html b/ietf/templates/nomcom/view_feedback.html index 197e91964..dc1912ee9 100644 --- a/ietf/templates/nomcom/view_feedback.html +++ b/ietf/templates/nomcom/view_feedback.html @@ -13,20 +13,24 @@ - + {% for ft in feedback_types %} - + {% endfor %} - {% for nominee, feedback in nominees_feedback.items %} + {% for nominee, feedback in nominees_feedback.items %} {% for f in feedback %} - + {% endfor %} {% endfor %} diff --git a/ietf/templates/nomcom/view_feedback_nominee.html b/ietf/templates/nomcom/view_feedback_nominee.html index c19b3477e..2ce9c9877 100644 --- a/ietf/templates/nomcom/view_feedback_nominee.html +++ b/ietf/templates/nomcom/view_feedback_nominee.html @@ -23,7 +23,7 @@ {% if feedback.type.slug == ft.slug %} {% if forloop.first %}

{% else %}
{% endif %}
-
From
+
{% if feedback.time > last_seen_time %}New{% endif %}From
{{ feedback.author|formatted_email|default:"Anonymous" }} {% if ft.slug == "nomina" and feedback.nomination_set.first.share_nominator %} OK to share name with nominee From b653e8fe8adce951bec9c597ccf2aafe51a40216 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 9 Dec 2015 23:24:46 +0000 Subject: [PATCH 14/37] Removed the description field from Position. Simplified the Position list and the Position edit form. Tweaked several places to make the pages more self documenting. Added a page to help nomcom chairs through setting up a new nomcom. Fixes #1867 and #1768. - Legacy-Id: 10565 --- ietf/nomcom/factories.py | 1 - ietf/nomcom/forms.py | 7 +- .../migrations/0008_auto_20151209_1423.py | 24 +++++ ietf/nomcom/models.py | 3 +- ietf/nomcom/resources.py | 1 - ietf/nomcom/test_data.py | 29 +++-- ietf/nomcom/tests.py | 14 ++- ietf/nomcom/urls.py | 1 + ietf/nomcom/views.py | 6 +- ietf/templates/nomcom/chair_help.html | 101 ++++++++++++++++++ ietf/templates/nomcom/list_positions.html | 17 +-- .../templates/nomcom/nomcom_private_base.html | 1 + ietf/templates/nomcom/questionnaires.html | 1 - ietf/templates/nomcom/remove_position.html | 2 - ietf/templates/nomcom/requirements.html | 1 - 15 files changed, 173 insertions(+), 36 deletions(-) create mode 100644 ietf/nomcom/migrations/0008_auto_20151209_1423.py create mode 100644 ietf/templates/nomcom/chair_help.html diff --git a/ietf/nomcom/factories.py b/ietf/nomcom/factories.py index 02791b872..72bff7355 100644 --- a/ietf/nomcom/factories.py +++ b/ietf/nomcom/factories.py @@ -125,7 +125,6 @@ class PositionFactory(factory.DjangoModelFactory): model = Position name = factory.Faker('sentence',nb_words=10) - description = factory.Faker('paragraph',nb_sentences=4) is_open = True class NomineeFactory(factory.DjangoModelFactory): diff --git a/ietf/nomcom/forms.py b/ietf/nomcom/forms.py index 11c7f9c43..82559716b 100644 --- a/ietf/nomcom/forms.py +++ b/ietf/nomcom/forms.py @@ -17,7 +17,6 @@ from ietf.nomcom.utils import (NOMINATION_RECEIPT_TEMPLATE, FEEDBACK_RECEIPT_TEM get_user_email, validate_private_key, validate_public_key, get_or_create_nominee, create_feedback_email) from ietf.person.models import Email -from ietf.person.fields import SearchableEmailField from ietf.utils.fields import MultiEmailField from ietf.utils.mail import send_mail from ietf.mailtrigger.utils import gather_address_lists @@ -566,13 +565,11 @@ class NomComTemplateForm(BaseNomcomForm, DBTemplateForm): class PositionForm(BaseNomcomForm, forms.ModelForm): - fieldsets = [('Position', ('name', 'description', 'is_open' ))] - - incumbent = SearchableEmailField(required=False) + fieldsets = [('Position', ('name', 'is_open' ))] class Meta: model = Position - fields = ('name', 'description', 'is_open') + fields = ('name', 'is_open') def __init__(self, *args, **kwargs): self.nomcom = kwargs.pop('nomcom', None) diff --git a/ietf/nomcom/migrations/0008_auto_20151209_1423.py b/ietf/nomcom/migrations/0008_auto_20151209_1423.py new file mode 100644 index 000000000..8d8861a6b --- /dev/null +++ b/ietf/nomcom/migrations/0008_auto_20151209_1423.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('nomcom', '0007_feedbacklastseen'), + ] + + operations = [ + migrations.RemoveField( + model_name='position', + name='description', + ), + migrations.AlterField( + model_name='position', + name='name', + field=models.CharField(help_text=b'This short description will appear on the Nomination and Feedback pages. Be as descriptive as necessary. Past examples: "Transport AD", "IAB Member"', max_length=255, verbose_name=b'Name'), + preserve_default=True, + ), + ] diff --git a/ietf/nomcom/models.py b/ietf/nomcom/models.py index 97247d860..521cbe233 100644 --- a/ietf/nomcom/models.py +++ b/ietf/nomcom/models.py @@ -164,8 +164,7 @@ class NomineePosition(models.Model): class Position(models.Model): nomcom = models.ForeignKey('NomCom') - name = models.CharField(verbose_name='Name', max_length=255) - description = models.TextField(verbose_name='Description') + 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) questionnaire = models.ForeignKey(DBTemplate, related_name='questionnaire', null=True, editable=False) is_open = models.BooleanField(verbose_name='Is open', default=False) diff --git a/ietf/nomcom/resources.py b/ietf/nomcom/resources.py index 19583bb12..0ab7115a5 100644 --- a/ietf/nomcom/resources.py +++ b/ietf/nomcom/resources.py @@ -37,7 +37,6 @@ class PositionResource(ModelResource): filtering = { "id": ALL, "name": ALL, - "description": ALL, "is_open": ALL, "nomcom": ALL_WITH_RELATIONS, "requirement": ALL_WITH_RELATIONS, diff --git a/ietf/nomcom/test_data.py b/ietf/nomcom/test_data.py index f680809dc..bee016d35 100644 --- a/ietf/nomcom/test_data.py +++ b/ietf/nomcom/test_data.py @@ -21,19 +21,19 @@ SECRETARIAT_USER = 'secretary' EMAIL_DOMAIN = '@example.com' NOMCOM_YEAR = "2013" -POSITIONS = { - "GEN": "IETF Chair/Gen AD", - "APP": "APP Area Director", - "INT": "INT Area Director", - "OAM": "OPS Area Director", - "OPS": "OPS Area Director", - "RAI": "RAI Area Director", - "RTG": "RTG Area Director", - "SEC": "SEC Area Director", - "TSV": "TSV Area Director", - "IAB": "IAB Member", - "IAOC": "IAOC Member", - } +POSITIONS = [ + "GEN", + "APP", + "INT", + "OAM", + "OPS", + "RAI", + "RTG", + "SEC", + "TSV", + "IAB", + "IAOC" + ] def generate_cert(): @@ -127,10 +127,9 @@ def nomcom_test_data(): nominee, _ = Nominee.objects.get_or_create(email=email, nomcom=nomcom) # positions - for name, description in POSITIONS.iteritems(): + for name in POSITIONS: position, created = Position.objects.get_or_create(nomcom=nomcom, name=name, - description=description, is_open=True) ChangeStateGroupEvent.objects.get_or_create(group=group, diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py index 9de16385c..30299c12c 100644 --- a/ietf/nomcom/tests.py +++ b/ietf/nomcom/tests.py @@ -430,7 +430,7 @@ class NomcomViewsTest(TestCase): nomcom = get_nomcom_by_year(self.year) count = nomcom.position_set.all().count() login_testing_unauthorized(self, CHAIR_USER, self.edit_position_url) - test_data = {"action" : "add", "name": "testpos", "description": "test description"} + test_data = {"action" : "add", "name": "testpos" } r = self.client.post(self.edit_position_url, test_data) self.assertEqual(r.status_code, 302) self.assertEqual(nomcom.position_set.all().count(), count+1) @@ -1181,3 +1181,15 @@ class FeedbackLastSeenTests(TestCase): self.assertEqual(response.status_code,200) q = PyQuery(response.content) self.assertEqual( len(q('.label-success')), 0 ) + +class NewActiveNomComTests(TestCase): + + def setUp(self): + self.nc = NomComFactory.create(**nomcom_kwargs_for_year(group__state_id='conclude')) + self.chair = self.nc.group.role_set.filter(name='chair').first().person + + def test_help(self): + url = reverse('nomcom_chair_help',kwargs={'year':self.nc.year()}) + login_testing_unauthorized(self, self.chair.user.username, url) + response = self.client.get(url) + self.assertEqual(response.status_code,200) diff --git a/ietf/nomcom/urls.py b/ietf/nomcom/urls.py index 15004d79f..e213f8871 100644 --- a/ietf/nomcom/urls.py +++ b/ietf/nomcom/urls.py @@ -7,6 +7,7 @@ urlpatterns = patterns('ietf.nomcom.views', url(r'^ann/$', 'announcements'), url(r'^(?P\d{4})/private/$', 'private_index', name='nomcom_private_index'), url(r'^(?P\d{4})/private/key/$', 'private_key', name='nomcom_private_key'), + url(r'^(?P\d{4})/private/help/$', 'configuration_help', name='nomcom_chair_help'), url(r'^(?P\d{4})/private/nominate/$', 'private_nominate', name='nomcom_private_nominate'), url(r'^(?P\d{4})/private/feedback/$', 'private_feedback', name='nomcom_private_feedback'), url(r'^(?P\d{4})/private/feedback-email/$', 'private_feedback_email', name='nomcom_private_feedback_email'), diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py index 3df144ba1..05aa4a8c1 100644 --- a/ietf/nomcom/views.py +++ b/ietf/nomcom/views.py @@ -874,7 +874,7 @@ def edit_template(request, year, template_id): @role_required("Nomcom Chair", "Nomcom Advisor") def list_positions(request, year): nomcom = get_nomcom_by_year(year) - positions = nomcom.position_set.all() + positions = nomcom.position_set.order_by('-is_open') return render_to_response('nomcom/list_positions.html', {'positions': positions, @@ -936,3 +936,7 @@ def edit_position(request, year, position_id=None): 'nomcom': nomcom, 'is_chair_task' : True, }, RequestContext(request)) + +@role_required("Nomcom Chair", "Nomcom Advisor") +def configuration_help(request, year): + return render(request,'nomcom/chair_help.html',{'year':year}) diff --git a/ietf/templates/nomcom/chair_help.html b/ietf/templates/nomcom/chair_help.html new file mode 100644 index 000000000..aab6c6092 --- /dev/null +++ b/ietf/templates/nomcom/chair_help.html @@ -0,0 +1,101 @@ +{% extends "nomcom/nomcom_private_base.html" %} +{# Copyright The IETF Trust 2015, All Rights Reserved #} +{% load origin %} + +{% load bootstrap3 %} +{% load ietf_filters %} + +{% block bodyAttrs %}data-spy="scroll" data-target="#nav-instructions"{% endblock %} + +{% block subtitle %} - Configuration Help {% endblock %} + +{% block nomcom_content %} + {% origin %} + + {% comment %} Why isn't this part of the base templates? {% endcomment %} + {% bootstrap_messages %} + + + +
+

Help for Configuring a New NomCom

+ + + + +

Generate a keypair for the nomcom

+ +

The Datatracker uses a public/private keypair to encrypt any feedback entered by the community +before storing it in the database. Only persons with the private key can view this feedback. +The private key is provided by using a datatracker page to store a blowfish-encrypted cookie in a browser session. +The blowfish-encrypted private key is sent to the server and used to decrypt feedback. The private key is never +stored on the server, but if the server is compromised, it would be possible for an attacker to grab the private key +by modifying the datatracker code. The NomCom chair generates the keypair for each NomCom and manages its secure +distribution. +

+ +

To generate the keypair: +

    +
  1. +Create a config file for openssl, named nomcom-config.cnf, with the following contents: +
    [ req ]
    +distinguished_name = req_distinguished_name
    +string_mask        = utf8only
    +x509_extensions    = ss_v3_ca
    +
    +[ req_distinguished_name ]
    +commonName           = Common Name (e.g. NomComYY)
    +commonName_default   = NomCom{{year|slice:"2:"}}
    +
    +[ ss_v3_ca ]
    +
    +subjectKeyIdentifier = hash
    +keyUsage = critical, digitalSignature, keyEncipherment, dataEncipherment
    +basicConstraints = critical, CA:true
    +subjectAltName = email:nomcom{{year|slice:"2:"}}@ietf.org
    +extendedKeyUsage= emailProtection
    +
  2. +
  3. Generate a private key and corresponding certificate: +
    openssl req -config nomcom-config.cnf -x509 -new -newkey rsa:2048 -sha256 -days 730 -nodes -keyout privateKey-nomcom{{year|slice:"2:"}}.pem -out nomcom{{year|slice:"2:"}}.cert
    +(Just press Enter when presented with "Common Name (e.g. NomComYY) [NomCom15]:") +
  4. +
+

+You will upload the certificate to the datatracker (and make it available to people wishing to send mail) in the steps below. +

+

Securely distribute privateKey-nomcom{{year|slice:"2:"}} to your NomCom advisor(s), liaisons, and members, as they become known.

+ + +

Configure the Datatracker NomCom

+ +

Sign into the datatracker and go to the NomCom Configuration Page.

+

Use the Browse button to select the public nomcom{{year|slice:"2:"}}.cert file created above.

+

Enter any special instructions you want to appear on the nomination entry form in the "Help text for nomination form" box. These will appear on the form immediately below the field labeled "Candidate's qualifications for the position".

+

Choose whether to have the datatracker send questionnares, and whether to automatically remind people to accept nominations and return questionnaires, according to the instructions on the form.

+

Press the save button.

+

You can return to this page and change your mind on any of the settings, even towards the end of your nomcom cycle. However, be wary of uploading a new public key once one feedback has been received. That step should only be taken in the case of a compromised keypair. Old feedback will remain encrypted with the old key, and will not be accessible through the datatracker.

+ +

Configure the Positions to be filled

+

Add the positions this nomcom needs to fill.

+

Only create one Position for those roles having multiple seats to fill, +such as the IAB, or the IESG areas where multiple ADs in that area are at the end of their term.

+ +

Note the "Is open" checkbox. When this is set to True, the Position will appear on the Nomination and Feedback pages. You will set this to False when you do not want any more feedback (that is, the position is filled, or otherwise closed for some reason). It is a good idea to start with the "Is open" value set to False. After you edit the templates for the position and are ready for the community to provide nominations and feedback, set the "Is open" value to True.

+

You might need to close some positions and open others as your nomcom progresses. For example, the 2014 Nomcom was called back after it had finished work on its usual selections to fill a IAOC position that had been vacated mid-term. Before making the call for nominations and feedback for this additional IAOC position, the chair would mark the already filled positions as not open, leaving only the new IAOC position open for consideration. At that point, only that IAOC position would be available on the Nomination and Feedback pages.

+ +

Customize the web-form and email templates

+ +

Edit each of the templates at {% url 'nomcom_list_templates' year %}. The "Home page of group" template is where to put information about the current nomcom members and policies. It is also a good place to list incumbents in positions, and information about whether the incumbents will stand again. See the home page of past nomcoms for examples.

+ +

Test the results

+

Before advertising that your nomcom pages are ready for the community to use, test your configuration. Create a dummy nominee for at least one position, and give it some feedback. You will be able to move this out of the way later. Once you've marked positions as open, ask your nomcom members to look over the expertise and questionnaires tab (which show rendered view of each of the templates for each position) to ensure they contain what you want the community to see. Please don't assume that everything is all right without looking. It's a good idea to give the secretariat and the tools team a heads up a few (preferably 3 to 5) days notice before announcing that your pages are ready for community use. +

+{% endblock %} + diff --git a/ietf/templates/nomcom/list_positions.html b/ietf/templates/nomcom/list_positions.html index b0106e9be..a53a16c60 100644 --- a/ietf/templates/nomcom/list_positions.html +++ b/ietf/templates/nomcom/list_positions.html @@ -10,16 +10,18 @@ {% if nomcom.group.state_id == 'active' %} Add new position +

{% endif %} {% if positions %} - {% for position in positions %} -

{{ position.name }}

+ {% regroup positions by is_open as posgroups %} + {% for group in posgroups %} +
+

{{ group.grouper| yesno:"Open Positions,Closed Positions"}}

+
+ {% for position in group.list %} +

{{ position.name }}

-
Description
-
{{ position.description }}
-
Is open
-
{{ position.is_open }}
Templates
{% for template in position.get_templates %} @@ -35,6 +37,9 @@ {% endif %}
{% endfor %} +
+
+ {% endfor %} {% else %}

There are no positions defined.

{% endif %} diff --git a/ietf/templates/nomcom/nomcom_private_base.html b/ietf/templates/nomcom/nomcom_private_base.html index a2464294b..20c4779c6 100644 --- a/ietf/templates/nomcom/nomcom_private_base.html +++ b/ietf/templates/nomcom/nomcom_private_base.html @@ -42,6 +42,7 @@ {% if nomcom.group.state_id == 'active' %}
  • Edit Members
  • {% endif %} +
  • Configuration Help
  • diff --git a/ietf/templates/nomcom/questionnaires.html b/ietf/templates/nomcom/questionnaires.html index 1716cf9a7..931d331c8 100644 --- a/ietf/templates/nomcom/questionnaires.html +++ b/ietf/templates/nomcom/questionnaires.html @@ -19,7 +19,6 @@
    {% for position in positions %}
    -

    {{ position.description }}

    {{ position.get_questionnaire|linebreaks}}
    {% endfor %} diff --git a/ietf/templates/nomcom/remove_position.html b/ietf/templates/nomcom/remove_position.html index 82b8f8235..166f5bf04 100644 --- a/ietf/templates/nomcom/remove_position.html +++ b/ietf/templates/nomcom/remove_position.html @@ -8,8 +8,6 @@ {% origin %}

    Position: {{ position }}

    -
    Description:
    -
    {{ position.description }}
    Is open:
    {{ position.is_open }}
    diff --git a/ietf/templates/nomcom/requirements.html b/ietf/templates/nomcom/requirements.html index df3b8a210..d6f2edcad 100644 --- a/ietf/templates/nomcom/requirements.html +++ b/ietf/templates/nomcom/requirements.html @@ -26,7 +26,6 @@
    {% for position in positions %}
    -

    {{ position.description }}

    {{ position.get_requirement|linebreaks}}
    {% endfor %} From 66e89e7d29512845cd39f5f49098cdb3c71b477e Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 10 Dec 2015 23:07:57 +0000 Subject: [PATCH 15/37] improved creation of FeedbackLastSeen - Legacy-Id: 10572 --- .../migrations/0007_feedbacklastseen.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/ietf/nomcom/migrations/0007_feedbacklastseen.py b/ietf/nomcom/migrations/0007_feedbacklastseen.py index 14f635a96..b387b0045 100644 --- a/ietf/nomcom/migrations/0007_feedbacklastseen.py +++ b/ietf/nomcom/migrations/0007_feedbacklastseen.py @@ -1,17 +1,35 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import datetime + from django.db import models, migrations +def create_lastseen(apps, schema_editor): + NomCom = apps.get_model('nomcom','NomCom') + FeedbackLastSeen = apps.get_model('nomcom','FeedbackLastSeen') + now = datetime.datetime.now() + for nc in NomCom.objects.all(): + reviewers = [r.person for r in nc.group.role_set.all()] + nominees = nc.nominee_set.all() + for r in reviewers: + for n in nominees: + FeedbackLastSeen.objects.create(reviewer=r,nominee=n,time=now) + +def remove_lastseen(apps, schema_editor): + FeedbackLastSeen = apps.get_model('nomcom','FeedbackLastSeen') + FeedbackLastSeen.objects.delete() class Migration(migrations.Migration): dependencies = [ ('person', '0004_auto_20150308_0440'), + ('group', '0006_auto_20150718_0509'), ('nomcom', '0006_improve_default_questionnaire_templates'), ] operations = [ + migrations.CreateModel( name='FeedbackLastSeen', fields=[ @@ -24,4 +42,7 @@ class Migration(migrations.Migration): }, bases=(models.Model,), ), + + migrations.RunPython(create_lastseen,remove_lastseen) + ] From 3bc615883db0158ee762a284011c0a7cae4ca1d6 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Fri, 11 Dec 2015 04:01:11 +0000 Subject: [PATCH 16/37] normalizes management of a test directory for test nomcom public keys - Legacy-Id: 10574 --- ietf/nomcom/tests.py | 51 ++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py index 30299c12c..55b00e887 100644 --- a/ietf/nomcom/tests.py +++ b/ietf/nomcom/tests.py @@ -43,6 +43,14 @@ def get_cert_files(): client_test_cert_files = generate_cert() return client_test_cert_files +def build_test_public_keys_dir(obj): + obj.nomcom_public_keys_dir = os.path.abspath("tmp-nomcom-public-keys-dir") + if not os.path.exists(obj.nomcom_public_keys_dir): + os.mkdir(obj.nomcom_public_keys_dir) + settings.NOMCOM_PUBLIC_KEYS_DIR = obj.nomcom_public_keys_dir + +def clean_test_public_keys_dir(obj): + shutil.rmtree(obj.nomcom_public_keys_dir) class NomcomViewsTest(TestCase): """Tests to create a new nomcom""" @@ -53,11 +61,7 @@ class NomcomViewsTest(TestCase): return response def setUp(self): - self.nomcom_public_keys_dir = os.path.abspath("tmp-nomcom-public-keys-dir") - if not os.path.exists(self.nomcom_public_keys_dir): - os.mkdir(self.nomcom_public_keys_dir) - settings.NOMCOM_PUBLIC_KEYS_DIR = self.nomcom_public_keys_dir - + build_test_public_keys_dir(self) nomcom_test_data() self.cert_file, self.privatekey_file = get_cert_files() self.year = NOMCOM_YEAR @@ -81,7 +85,7 @@ class NomcomViewsTest(TestCase): self.public_nominate_url = reverse('nomcom_public_nominate', kwargs={'year': self.year}) def tearDown(self): - shutil.rmtree(self.nomcom_public_keys_dir) + clean_test_public_keys_dir(self) def access_member_url(self, url): login_testing_unauthorized(self, COMMUNITY_USER, url) @@ -753,16 +757,12 @@ class NomineePositionStateSaveTest(TestCase): """Tests for the NomineePosition save override method""" def setUp(self): - self.nomcom_public_keys_dir = os.path.abspath("tmp-nomcom-public-keys-dir") - if not os.path.exists(self.nomcom_public_keys_dir): - os.mkdir(self.nomcom_public_keys_dir) - settings.NOMCOM_PUBLIC_KEYS_DIR = self.nomcom_public_keys_dir - + build_test_public_keys_dir(self) nomcom_test_data() self.nominee = Nominee.objects.get(email__person__user__username=COMMUNITY_USER) def tearDown(self): - shutil.rmtree(self.nomcom_public_keys_dir) + clean_test_public_keys_dir(self) def test_state_autoset(self): """Verify state is autoset correctly""" @@ -792,16 +792,13 @@ class NomineePositionStateSaveTest(TestCase): class FeedbackTest(TestCase): def setUp(self): - self.nomcom_public_keys_dir = os.path.abspath("tmp-nomcom-public-keys-dir") - if not os.path.exists(self.nomcom_public_keys_dir): - os.mkdir(self.nomcom_public_keys_dir) - settings.NOMCOM_PUBLIC_KEYS_DIR = self.nomcom_public_keys_dir + build_test_public_keys_dir(self) nomcom_test_data() self.cert_file, self.privatekey_file = get_cert_files() def tearDown(self): - shutil.rmtree(self.nomcom_public_keys_dir) + clean_test_public_keys_dir(self) def test_encrypted_comments(self): @@ -828,11 +825,7 @@ class FeedbackTest(TestCase): class ReminderTest(TestCase): def setUp(self): - self.nomcom_public_keys_dir = os.path.abspath("tmp-nomcom-public-keys-dir") - if not os.path.exists(self.nomcom_public_keys_dir): - os.mkdir(self.nomcom_public_keys_dir) - settings.NOMCOM_PUBLIC_KEYS_DIR = self.nomcom_public_keys_dir - + build_test_public_keys_dir(self) nomcom_test_data() self.nomcom = get_nomcom_by_year(NOMCOM_YEAR) self.cert_file, self.privatekey_file = get_cert_files() @@ -872,7 +865,7 @@ class ReminderTest(TestCase): feedback.nominees.add(n) def tearDown(self): - shutil.rmtree(self.nomcom_public_keys_dir) + clean_test_public_keys_dir(self) def test_is_time_to_send(self): self.nomcom.reminder_interval = 4 @@ -928,11 +921,15 @@ class ReminderTest(TestCase): class InactiveNomcomTests(TestCase): def setUp(self): + build_test_public_keys_dir(self) self.nc = NomComFactory.create(**nomcom_kwargs_for_year(group__state_id='conclude')) self.plain_person = PersonFactory.create() self.chair = self.nc.group.role_set.filter(name='chair').first().person self.member = self.nc.group.role_set.filter(name='member').first().person + def tearDown(self): + clean_test_public_keys_dir(self) + def test_feedback_closed(self): for view in ['nomcom_public_feedback', 'nomcom_private_feedback']: url = reverse(view, kwargs={'year': self.nc.year()}) @@ -1118,6 +1115,7 @@ class InactiveNomcomTests(TestCase): class FeedbackLastSeenTests(TestCase): def setUp(self): + build_test_public_keys_dir(self) self.nc = NomComFactory.create(**nomcom_kwargs_for_year(group__state_id='conclude')) self.author = PersonFactory.create().email_set.first().address self.member = self.nc.group.role_set.filter(name='member').first().person @@ -1132,6 +1130,9 @@ class FeedbackLastSeenTests(TestCase): self.half_hour_ago = now - datetime.timedelta(minutes=30) self.second_from_now = now + datetime.timedelta(seconds=1) + def tearDown(self): + clean_test_public_keys_dir(self) + def test_feedback_index_badges(self): url = reverse('nomcom_view_feedback',kwargs={'year':self.nc.year()}) login_testing_unauthorized(self, self.member.user.username, url) @@ -1185,9 +1186,13 @@ class FeedbackLastSeenTests(TestCase): class NewActiveNomComTests(TestCase): def setUp(self): + build_test_public_keys_dir(self) self.nc = NomComFactory.create(**nomcom_kwargs_for_year(group__state_id='conclude')) self.chair = self.nc.group.role_set.filter(name='chair').first().person + def tearDown(self): + clean_test_public_keys_dir(self) + def test_help(self): url = reverse('nomcom_chair_help',kwargs={'year':self.nc.year()}) login_testing_unauthorized(self, self.chair.user.username, url) From 6574618cdfb70e1b2cd92eb94839110bc6a64908 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Fri, 11 Dec 2015 05:06:58 +0000 Subject: [PATCH 17/37] a stab in the dark at initializing the public_key in a test friendly way - Legacy-Id: 10575 --- ietf/nomcom/factories.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ietf/nomcom/factories.py b/ietf/nomcom/factories.py index 72bff7355..07af82ee7 100644 --- a/ietf/nomcom/factories.py +++ b/ietf/nomcom/factories.py @@ -1,6 +1,9 @@ import factory import random +from django.core.files.storage import FileSystemStorage +from django.conf import settings + from ietf.nomcom.models import NomCom, Position, Feedback, Nominee, NomineePosition from ietf.group.factories import GroupFactory from ietf.person.factories import PersonFactory @@ -81,7 +84,7 @@ class NomComFactory(factory.DjangoModelFactory): group = factory.SubFactory(GroupFactory,type_id='nomcom') - public_key = factory.django.FileField(data=cert) + public_key = factory.django.FileField(data=cert,storage=FileSystemStorage(location=settings.NOMCOM_PUBLIC_KEYS_DIR)) @factory.post_generation def populate_positions(self, create, extracted, **kwargs): From ea6d82846f72ae1e7666650900c7a0298d38c1ee Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Fri, 11 Dec 2015 17:30:13 +0000 Subject: [PATCH 18/37] Avoiding an initialization problem by setting NOMCOM_PUBLIC_KEYS_DIR in settings_sqlitetest. This is not the best long term fix - Legacy-Id: 10581 --- ietf/nomcom/factories.py | 2 +- ietf/nomcom/tests.py | 4 ++-- ietf/settings_sqlitetest.py | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ietf/nomcom/factories.py b/ietf/nomcom/factories.py index 07af82ee7..a22f857f6 100644 --- a/ietf/nomcom/factories.py +++ b/ietf/nomcom/factories.py @@ -84,7 +84,7 @@ class NomComFactory(factory.DjangoModelFactory): group = factory.SubFactory(GroupFactory,type_id='nomcom') - public_key = factory.django.FileField(data=cert,storage=FileSystemStorage(location=settings.NOMCOM_PUBLIC_KEYS_DIR)) + public_key = factory.django.FileField(data=cert) @factory.post_generation def populate_positions(self, create, extracted, **kwargs): diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py index 55b00e887..9b8452333 100644 --- a/ietf/nomcom/tests.py +++ b/ietf/nomcom/tests.py @@ -1116,7 +1116,7 @@ class FeedbackLastSeenTests(TestCase): def setUp(self): build_test_public_keys_dir(self) - self.nc = NomComFactory.create(**nomcom_kwargs_for_year(group__state_id='conclude')) + self.nc = NomComFactory.create(**nomcom_kwargs_for_year()) self.author = PersonFactory.create().email_set.first().address self.member = self.nc.group.role_set.filter(name='member').first().person self.nominee = self.nc.nominee_set.first() @@ -1187,7 +1187,7 @@ class NewActiveNomComTests(TestCase): def setUp(self): build_test_public_keys_dir(self) - self.nc = NomComFactory.create(**nomcom_kwargs_for_year(group__state_id='conclude')) + self.nc = NomComFactory.create(**nomcom_kwargs_for_year()) self.chair = self.nc.group.role_set.filter(name='chair').first().person def tearDown(self): diff --git a/ietf/settings_sqlitetest.py b/ietf/settings_sqlitetest.py index 82aa8b2be..1e7bc59a6 100644 --- a/ietf/settings_sqlitetest.py +++ b/ietf/settings_sqlitetest.py @@ -5,6 +5,7 @@ # ./manage.py test --settings=settings_sqlitetest doc.ChangeStateTestCase # +import io from settings import * # pyflakes:ignore # Workaround to avoid spending minutes stepping through the migrations in @@ -38,4 +39,5 @@ DATABASES = { if TEST_CODE_COVERAGE_CHECKER and not TEST_CODE_COVERAGE_CHECKER._started: TEST_CODE_COVERAGE_CHECKER.start() - + +NOMCOM_PUBLIC_KEYS_DIR=os.path.abspath("tmp-nomcom-public-keys-dir") From d2f1d1ff12fd30c4d444d889caad6f9a2751cf7d Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Fri, 11 Dec 2015 19:49:33 +0000 Subject: [PATCH 19/37] corrected an improper import and removed some flakes - Legacy-Id: 10582 --- ietf/nomcom/factories.py | 3 --- ietf/settings_sqlitetest.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/ietf/nomcom/factories.py b/ietf/nomcom/factories.py index a22f857f6..72bff7355 100644 --- a/ietf/nomcom/factories.py +++ b/ietf/nomcom/factories.py @@ -1,9 +1,6 @@ import factory import random -from django.core.files.storage import FileSystemStorage -from django.conf import settings - from ietf.nomcom.models import NomCom, Position, Feedback, Nominee, NomineePosition from ietf.group.factories import GroupFactory from ietf.person.factories import PersonFactory diff --git a/ietf/settings_sqlitetest.py b/ietf/settings_sqlitetest.py index 1e7bc59a6..3a2f2587a 100644 --- a/ietf/settings_sqlitetest.py +++ b/ietf/settings_sqlitetest.py @@ -5,7 +5,7 @@ # ./manage.py test --settings=settings_sqlitetest doc.ChangeStateTestCase # -import io +import os from settings import * # pyflakes:ignore # Workaround to avoid spending minutes stepping through the migrations in From 015242da28a1cf8d5be85577859591f96ebf5828 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Fri, 11 Dec 2015 20:56:52 +0000 Subject: [PATCH 20/37] Improve guidance on when, and when not, to delete a Position - Legacy-Id: 10584 --- ietf/templates/nomcom/remove_position.html | 26 ++++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/ietf/templates/nomcom/remove_position.html b/ietf/templates/nomcom/remove_position.html index 166f5bf04..03e2bb395 100644 --- a/ietf/templates/nomcom/remove_position.html +++ b/ietf/templates/nomcom/remove_position.html @@ -7,12 +7,24 @@ {% block nomcom_content %} {% origin %}

    Position: {{ position }}

    -
    -
    Is open:
    -
    {{ position.is_open }}
    -
    -

    Do you want to remove it?

    +

    This position is currently {{position.is_open|yesno:"open,closed"}}.

    +

    It has {{position.feedback_set.count|default:"no"}} feedback objects associated with it.

    + {% if position.feedback_set.count %} +

    + Unless this is a position created only for testing, deleting it is likely to be harmful. All of the feedback will also be deleted. +

    +

    + {% if position.is_open %} + If you are just wanting the position to disappear from the lists available to the community for providing nominations and feedback, instead of deleting the position, edit the position and change is_open to False. + {% else %} + Since the position is closed, it will not appear on the lists available to the community for providing nominations and feedback. + {% endif %} +

    +

    If this is just a test position, it is ok to delete it.

    + {% else %} +

    This position is safe to delete.

    + {% endif %}
    {% csrf_token %} @@ -20,8 +32,8 @@ {% buttons %} - No, get me out of here - + + Cancel {% endbuttons %} From 7d120da9efefb58cc86dc011aa68cf49d68df7f1 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Sat, 12 Dec 2015 03:49:23 +0000 Subject: [PATCH 21/37] Removed the type from the template pathname for the requirements templates. Made the requiremetns view work for both template types plain and rst. Changed the default for the requirements template for new nomcoms to rst. Migrated 2015 requirements to rst (except for the IAB position, which would require a significant edit) - Legacy-Id: 10587 --- ietf/dbtemplate/fixtures/nomcom_templates.xml | 4 +- ..._requirements_dbtemplate_type_from_path.py | 50 +++++++++++++++++++ ietf/nomcom/models.py | 6 ++- ietf/nomcom/utils.py | 2 +- ietf/templates/nomcom/requirements.html | 2 +- 5 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 ietf/nomcom/migrations/0009_remove_requirements_dbtemplate_type_from_path.py diff --git a/ietf/dbtemplate/fixtures/nomcom_templates.xml b/ietf/dbtemplate/fixtures/nomcom_templates.xml index 1059d7073..cc0eb32c6 100644 --- a/ietf/dbtemplate/fixtures/nomcom_templates.xml +++ b/ietf/dbtemplate/fixtures/nomcom_templates.xml @@ -76,10 +76,10 @@ Questionnaire - /nomcom/defaults/position/requirements.txt + /nomcom/defaults/position/requirements Position requirements $position: Position - plain + rst These are the requirements for the position $position: Requirements. diff --git a/ietf/nomcom/migrations/0009_remove_requirements_dbtemplate_type_from_path.py b/ietf/nomcom/migrations/0009_remove_requirements_dbtemplate_type_from_path.py new file mode 100644 index 000000000..00830a534 --- /dev/null +++ b/ietf/nomcom/migrations/0009_remove_requirements_dbtemplate_type_from_path.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + + +def remove_extension(apps, schema_editor): + DBTemplate = apps.get_model('dbtemplate','DBTemplate') + for template in DBTemplate.objects.filter(path__endswith="requirements.txt"): + template.path = template.path[:-4] + template.save() + +def restore_extension(apps, schema_editor): + DBTemplate = apps.get_model('dbtemplate','DBTemplate') + for template in DBTemplate.objects.filter(path__endswith="requirements"): + template.path = template.path+".txt" + template.save() + +def default_rst(apps, schema_editor): + DBTemplate = apps.get_model('dbtemplate','DBTemplate') + default_req = DBTemplate.objects.get(path__startswith='/nomcom/defaults/position/requirements') + default_req.type_id = 'rst' + default_req.save() + +def default_plain(apps, schema_editor): + DBTemplate = apps.get_model('dbtemplate','DBTemplate') + default_req = DBTemplate.objects.get(path__startswith='/nomcom/defaults/position/requirements') + default_req.type_id = 'plain' + default_req.save() + +def rst_2015(apps, schema_editor): + DBTemplate = apps.get_model('dbtemplate','DBTemplate') + DBTemplate.objects.filter(path__startswith='/nomcom/nomcom2015/').filter(path__contains='position/requirements').exclude(path__contains='/27/').update(type_id='rst') + +def plain_2015(apps, schema_editor): + DBTemplate = apps.get_model('dbtemplate','DBTemplate') + DBTemplate.objects.filter(path__startswith='/nomcom/nomcom2015/').filter(path__contains='position/requirements').update(type_id='plain') + +class Migration(migrations.Migration): + + dependencies = [ + ('nomcom', '0008_auto_20151209_1423'), + ('dbtemplate', '0002_auto_20141222_1749'), + ] + + operations = [ + migrations.RunPython(remove_extension,restore_extension), + migrations.RunPython(default_rst,default_plain), + migrations.RunPython(rst_2015,plain_2015), + ] diff --git a/ietf/nomcom/models.py b/ietf/nomcom/models.py index 521cbe233..238249ed8 100644 --- a/ietf/nomcom/models.py +++ b/ietf/nomcom/models.py @@ -7,6 +7,7 @@ from django.conf import settings from django.core.files.storage import FileSystemStorage from django.contrib.auth.models import User from django.template.loader import render_to_string +from django.template.defaultfilters import linebreaks from ietf.nomcom.fields import EncryptedTextField from ietf.person.models import Person,Email @@ -201,7 +202,10 @@ class Position(models.Model): return render_to_string(self.questionnaire.path, {'position': self}) def get_requirement(self): - return render_to_string(self.requirement.path, {'position': self}) + rendered = render_to_string(self.requirement.path, {'position': self}) + if self.requirement.type_id=='plain': + rendered = linebreaks(rendered) + return rendered class Feedback(models.Model): diff --git a/ietf/nomcom/utils.py b/ietf/nomcom/utils.py index 55cdfa4fb..1349c175e 100644 --- a/ietf/nomcom/utils.py +++ b/ietf/nomcom/utils.py @@ -29,7 +29,7 @@ import debug # pyflakes:ignore MAIN_NOMCOM_TEMPLATE_PATH = '/nomcom/defaults/' QUESTIONNAIRE_TEMPLATE = 'position/questionnaire.txt' HEADER_QUESTIONNAIRE_TEMPLATE = 'position/header_questionnaire.txt' -REQUIREMENTS_TEMPLATE = 'position/requirements.txt' +REQUIREMENTS_TEMPLATE = 'position/requirements' HOME_TEMPLATE = 'home.rst' INEXISTENT_PERSON_TEMPLATE = 'email/inexistent_person.txt' NOMINEE_EMAIL_TEMPLATE = 'email/new_nominee.txt' diff --git a/ietf/templates/nomcom/requirements.html b/ietf/templates/nomcom/requirements.html index d6f2edcad..e1f6e9afa 100644 --- a/ietf/templates/nomcom/requirements.html +++ b/ietf/templates/nomcom/requirements.html @@ -26,7 +26,7 @@
    {% for position in positions %}
    - {{ position.get_requirement|linebreaks}} + {{ position.get_requirement|safe }}
    {% endfor %}
    From 9ddcd62c441d875ab93cb4c4358a1159e03d3e21 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Mon, 14 Dec 2015 03:26:43 +0000 Subject: [PATCH 22/37] Allow nominees to add a comment when accepting or declining. Fixes #1845. - Legacy-Id: 10589 --- ietf/nomcom/forms.py | 5 ++- ietf/nomcom/tests.py | 27 +++++++++++++ ietf/nomcom/views.py | 39 ++++++++++++++----- .../nomcom/process_nomination_status.html | 2 + 4 files changed, 63 insertions(+), 10 deletions(-) diff --git a/ietf/nomcom/forms.py b/ietf/nomcom/forms.py index 82559716b..0a59f346a 100644 --- a/ietf/nomcom/forms.py +++ b/ietf/nomcom/forms.py @@ -103,7 +103,6 @@ class BaseNomcomForm(object): continue yield fieldset_dict - class EditMembersForm(BaseNomcomForm, forms.Form): members = MultiEmailField(label="Members email", required=False, widget=forms.Textarea) @@ -767,3 +766,7 @@ class EditNomineeForm(forms.ModelForm): if nominees: raise forms.ValidationError('This emails already does exists in another nominee, please go to merge form') return nominee_email + +class NominationResponseCommentForm(forms.Form): + comments = forms.CharField(widget=forms.Textarea,required=False,help_text="Any comments provided will be encrytped and will only be visible to the NomCom.") + diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py index 9b8452333..efc64ba5b 100644 --- a/ietf/nomcom/tests.py +++ b/ietf/nomcom/tests.py @@ -1198,3 +1198,30 @@ class NewActiveNomComTests(TestCase): login_testing_unauthorized(self, self.chair.user.username, url) response = self.client.get(url) self.assertEqual(response.status_code,200) + + def test_accept_reject_nomination_comment(self): + np = self.nc.nominee_set.first().nomineeposition_set.first() + hash = get_hash_nominee_position(np.time.strftime("%Y%m%d"),np.id) + url = reverse('nomcom_process_nomination_status', + kwargs={'year':self.nc.year(), + 'nominee_position_id':np.id, + 'state':'accepted', + 'date':np.time.strftime("%Y%m%d"), + 'hash':hash, + } + ) + np.state_id='pending' + np.save() + response = self.client.get(url) + self.assertEqual(response.status_code,200) + feedback_count_before = Feedback.objects.count() + response = self.client.post(url,{}) + # This view uses Yaco-style POST handling + self.assertEqual(response.status_code,200) + self.assertEqual(Feedback.objects.count(),feedback_count_before) + np.state_id='pending' + np.save() + response = self.client.post(url,{'comments':'A nonempty comment'}) + self.assertEqual(response.status_code,200) + self.assertEqual(Feedback.objects.count(),feedback_count_before+1) + diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py index 05aa4a8c1..c180de2ce 100644 --- a/ietf/nomcom/views.py +++ b/ietf/nomcom/views.py @@ -4,6 +4,7 @@ from collections import OrderedDict, Counter from django.conf import settings from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import AnonymousUser from django.contrib import messages from django.core.urlresolvers import reverse from django.http import Http404, HttpResponseRedirect, HttpResponseForbidden @@ -25,7 +26,7 @@ from ietf.nomcom.forms import (NominateForm, FeedbackForm, QuestionnaireForm, MergeForm, NomComTemplateForm, PositionForm, PrivateKeyForm, EditNomcomForm, EditNomineeForm, PendingFeedbackForm, ReminderDatesForm, FullFeedbackFormSet, - FeedbackEmailForm) + FeedbackEmailForm, NominationResponseCommentForm) from ietf.nomcom.models import Position, NomineePosition, Nominee, Feedback, NomCom, ReminderDates, FeedbackLastSeen from ietf.nomcom.utils import (get_nomcom_by_year, store_nomcom_private_key, get_hash_nominee_position, send_reminder_to_nominees, @@ -546,15 +547,34 @@ def process_nomination_status(request, year, nominee_position_id, state, date, h state = get_object_or_404(NomineePositionStateName, slug=state) message = ('warning', ("Click on 'Save' to set the state of your nomination to %s to %s (this"+ - "is not a final commitment - you can notify us later if you need to change this)") % + " is not a final commitment - you can notify us later if you need to change this).") % (nominee_position.position.name, state.name)) if request.method == 'POST': - nominee_position.state = state - nominee_position.save() - need_confirmation = False - message = message = ('success', 'Your nomination on %s has been set as %s' % (nominee_position.position.name, - state.name)) - + form = NominationResponseCommentForm(request.POST) + if form.is_valid(): + nominee_position.state = state + nominee_position.save() + need_confirmation = False + if form.cleaned_data['comments']: + # This Feedback object is of type comment instead of nomina in order to not + # make answering "who nominated themselves" harder. + who = request.user + if isinstance(who,AnonymousUser): + who = None + f = Feedback.objects.create(nomcom = nomcom, + author = nominee_position.nominee.email, + subject = '%s nomination %s'%(nominee_position.nominee.name(),state), + comments = form.cleaned_data['comments'], + type_id = 'comment', + user = who, + ) + f.positions.add(nominee_position.position) + f.nominees.add(nominee_position.nominee) + + message = ('success', 'Your nomination on %s has been set as %s' % (nominee_position.position.name, + state.name)) + else: + form = NominationResponseCommentForm() return render_to_response('nomcom/process_nomination_status.html', {'message': message, 'nomcom': nomcom, @@ -562,7 +582,8 @@ def process_nomination_status(request, year, nominee_position_id, state, date, h 'nominee_position': nominee_position, 'state': state, 'need_confirmation': need_confirmation, - 'selected': 'feedback'}, RequestContext(request)) + 'selected': 'feedback', + 'form': form }, RequestContext(request)) @role_required("Nomcom") diff --git a/ietf/templates/nomcom/process_nomination_status.html b/ietf/templates/nomcom/process_nomination_status.html index 9979e7fbf..a03a3422e 100644 --- a/ietf/templates/nomcom/process_nomination_status.html +++ b/ietf/templates/nomcom/process_nomination_status.html @@ -16,6 +16,8 @@
    {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} {% endbuttons %} From 5e0632e8eb01c27090d7becb49ac0e7b5caf7c26 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Mon, 14 Dec 2015 21:05:52 +0000 Subject: [PATCH 23/37] Regroup the view-feedback view to make it easier to see where to spend review effort. Fixes #1866. - Legacy-Id: 10592 --- ietf/nomcom/views.py | 19 ++++++- ietf/templates/nomcom/view_feedback.html | 66 +++++++++++++++--------- 2 files changed, 58 insertions(+), 27 deletions(-) diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py index c180de2ce..c24c56774 100644 --- a/ietf/nomcom/views.py +++ b/ietf/nomcom/views.py @@ -598,8 +598,23 @@ def view_feedback(request, year): feedback_types.append(ft) else: independent_feedback_types.append(ft) - nominees_feedback = {} + nominees_feedback = [] + + def nominee_staterank(nominee): + states=nominee.nomineeposition_set.values_list('state_id',flat=True) + if 'accepted' in states: + return 0 + elif 'pending' in states: + return 1 + else: + return 2 + for nominee in nominees: + nominee.staterank = nominee_staterank(nominee) + + sorted_nominees = sorted(nominees,key=lambda x:x.staterank) + + for nominee in sorted_nominees: last_seen = FeedbackLastSeen.objects.filter(reviewer=request.user.person,nominee=nominee).first() nominee_feedback = [] for ft in feedback_types: @@ -612,7 +627,7 @@ def view_feedback(request, year): else: newflag = qs.filter(time__gt=last_seen.time).exists() nominee_feedback.append( (ft.name,count,newflag) ) - nominees_feedback.update({nominee: nominee_feedback}) + nominees_feedback.append( {'nominee':nominee, 'feedback':nominee_feedback} ) independent_feedback = [ft.feedback_set.get_by_nomcom(nomcom).count() for ft in independent_feedback_types] return render_to_response('nomcom/view_feedback.html', diff --git a/ietf/templates/nomcom/view_feedback.html b/ietf/templates/nomcom/view_feedback.html index dc1912ee9..d048f9d87 100644 --- a/ietf/templates/nomcom/view_feedback.html +++ b/ietf/templates/nomcom/view_feedback.html @@ -10,32 +10,48 @@ {% origin %}

    Feedback related to nominees

    -
    NomineeNominee{{ ft.name }}{{ ft.name }}
    - {{ nominee }} + {{ nominee.name }} + {{ f.1 }} + {% if f.2 %}New{% endif %} + {{ f.1 }} +
    - - - - {% for ft in feedback_types %} - - {% endfor %} - - - - {% for nominee, feedback in nominees_feedback.items %} - - - {% for f in feedback %} - + {% regroup nominees_feedback by nominee.staterank as stateranked_nominees %} + {% for staterank in stateranked_nominees %} +
    +
    + {% if staterank.grouper == 0 %} + Accepted nomination for at least one position + {% elif staterank.grouper == 1 %} + Pending for at least one position and has not accepted any nomination + {% else %} + Declined each nominated position + {% endif %} +
    +
    +
    Nominee{{ ft.name }}
    - {{ nominee.name }} - - - {% if f.2 %}New{% endif %} - {{ f.1 }} -
    + + + + {% for ft in feedback_types %} + + {% endfor %} + + + + {% for fb_dict in staterank.list %} + + + {% for fbtype_name, fbtype_count, fbtype_newflag in fb_dict.feedback %} + + {% endfor %} + {% endfor %} - - {% endfor %} - -
    Nominee{{ ft.name }}
    + {{ fb_dict.nominee.name }} + + + {% if fbtype_newflag %}New{% endif %} + {{ fbtype_count }} +
    + + +
    + + {% endfor %} {% if independent_feedback_types %}

    Feedback not related to Nominees

    From bf54e2b3f5cf5e1223305e0a530973e285a25fa7 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Mon, 14 Dec 2015 23:14:44 +0000 Subject: [PATCH 24/37] Regroup multiselect options to make classifying pending feedback simpler. Make the control larger and resizable. Fixes #1854. - Legacy-Id: 10593 --- ietf/nomcom/forms.py | 12 +++++++++--- ietf/templates/nomcom/view_feedback_pending.html | 4 ++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/ietf/nomcom/forms.py b/ietf/nomcom/forms.py index 0a59f346a..854f22e3c 100644 --- a/ietf/nomcom/forms.py +++ b/ietf/nomcom/forms.py @@ -39,9 +39,15 @@ class PositionNomineeField(forms.ChoiceField): positions = Position.objects.get_by_nomcom(self.nomcom).opened().order_by('name') results = [] for position in positions: - nominees = [('%s_%s' % (position.id, i.id), unicode(i)) for i in Nominee.objects.get_by_nomcom(self.nomcom).not_duplicated().filter(nominee_position=position).select_related("email", "email__person")] + accepted_nominees = [np.nominee for np in NomineePosition.objects.filter(position=position,state='accepted').exclude(nominee__duplicated__isnull=False)] + nominees = [('%s_%s' % (position.id, i.id), unicode(i)) for i in accepted_nominees] if nominees: - results.append((position.name, nominees)) + results.append((position.name+" (Accepted)", nominees)) + for position in positions: + other_nominees = [np.nominee for np in NomineePosition.objects.filter(position=position).exclude(state='accepted').exclude(nominee__duplicated__isnull=False)] + nominees = [('%s_%s' % (position.id, i.id), unicode(i)) for i in other_nominees] + if nominees: + results.append((position.name+" (Declined or Pending)", nominees)) kwargs['choices'] = results super(PositionNomineeField, self).__init__(*args, **kwargs) @@ -671,7 +677,7 @@ class MutableFeedbackForm(forms.ModelForm): if self.feedback_type.slug != 'nomina': self.fields['nominee'] = MultiplePositionNomineeField(nomcom=self.nomcom, required=True, - widget=forms.SelectMultiple, + 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.') else: self.fields['position'] = forms.ModelChoiceField(queryset=Position.objects.get_by_nomcom(self.nomcom).opened(), label="Position") diff --git a/ietf/templates/nomcom/view_feedback_pending.html b/ietf/templates/nomcom/view_feedback_pending.html index 9a418a1bd..7302b7008 100644 --- a/ietf/templates/nomcom/view_feedback_pending.html +++ b/ietf/templates/nomcom/view_feedback_pending.html @@ -7,6 +7,10 @@ {% block subtitle %} - Feeback pending{% endblock %} +{% block morecss %} +.nominee_multi_select { resize: vertical; } +{% endblock %} + {% block nomcom_content %} {% origin %}

    Feedback pending from email list

    From df571c2fccc781497feef8667decfe0fc749ed28 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 16 Dec 2015 11:51:32 +0000 Subject: [PATCH 25/37] Improved test coverage (from 66 to 87%) - checkpointing before removing move_to_default - Legacy-Id: 10602 --- ietf/nomcom/factories.py | 20 ++-- ietf/nomcom/tests.py | 218 ++++++++++++++++++++++++++++++++++++++- ietf/nomcom/views.py | 2 +- 3 files changed, 229 insertions(+), 11 deletions(-) diff --git a/ietf/nomcom/factories.py b/ietf/nomcom/factories.py index 72bff7355..049e32f62 100644 --- a/ietf/nomcom/factories.py +++ b/ietf/nomcom/factories.py @@ -92,18 +92,20 @@ class NomComFactory(factory.DjangoModelFactory): if extracted is None: extracted = True if create and extracted: - nominees = [Nominee.objects.create(nomcom=self, email=PersonFactory().email_set.first()) for i in range(2)] + nominees = [Nominee.objects.create(nomcom=self, email=PersonFactory().email_set.first()) for i in range(4)] positions = [PositionFactory(nomcom=self) for i in range(3)] - def npc(x,y): - return NomineePosition.objects.create(position=x, - nominee=y, - state_id='accepted') + def npc(position,nominee,state_id): + return NomineePosition.objects.create(position=position, + nominee=nominee, + state_id=state_id) # This gives us positions with 0, 1 and 2 nominees, and - # one person who's been nomminated for more than one position - npc(positions[0],nominees[0]) - npc(positions[1],nominees[0]) - npc(positions[1],nominees[1]) + # one person who's been nominated for more than one position + npc(positions[0],nominees[0],'accepted') + npc(positions[1],nominees[0],'accepted') + npc(positions[1],nominees[1],'accepted') + npc(positions[0],nominees[2],'pending') + npc(positions[0],nominees[3],'declined') @factory.post_generation def populate_personnel(self, create, extracted, **kwargs): diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py index efc64ba5b..a22088e85 100644 --- a/ietf/nomcom/tests.py +++ b/ietf/nomcom/tests.py @@ -31,7 +31,8 @@ from ietf.nomcom.utils import get_nomcom_by_year, get_or_create_nominee, get_has from ietf.nomcom.management.commands.send_reminders import Command, is_time_to_send from ietf.nomcom.factories import NomComFactory, FeedbackFactory, \ - nomcom_kwargs_for_year, provide_private_key_to_test_client + nomcom_kwargs_for_year, provide_private_key_to_test_client, \ + key from ietf.person.factories import PersonFactory from ietf.dbtemplate.factories import DBTemplateFactory @@ -1199,6 +1200,37 @@ class NewActiveNomComTests(TestCase): response = self.client.get(url) self.assertEqual(response.status_code,200) + def test_accept_reject_nomination_edges(self): + + np = self.nc.nominee_set.first().nomineeposition_set.first() + + kwargs={'year':self.nc.year(), + 'nominee_position_id':np.id, + 'state':'accepted', + 'date':np.time.strftime("%Y%m%d"), + 'hash':get_hash_nominee_position(np.time.strftime("%Y%m%d"),np.id), + } + url = reverse('nomcom_process_nomination_status', kwargs=kwargs) + response = self.client.get(url) + self.assertEqual(response.status_code,403) + self.assertTrue('already was' in unicontent(response)) + + settings.DAYS_TO_EXPIRE_NOMINATION_LINK = 2 + np.time = np.time - datetime.timedelta(days=3) + np.save() + kwargs['date'] = np.time.strftime("%Y%m%d") + kwargs['hash'] = get_hash_nominee_position(np.time.strftime("%Y%m%d"),np.id) + url = reverse('nomcom_process_nomination_status', kwargs=kwargs) + response = self.client.get(url) + self.assertEqual(response.status_code,403) + self.assertTrue('Link expired' in unicontent(response)) + + kwargs['hash'] = 'bad' + url = reverse('nomcom_process_nomination_status', kwargs=kwargs) + response = self.client.get(url) + self.assertEqual(response.status_code,403) + self.assertTrue('Bad hash!' in unicontent(response)) + def test_accept_reject_nomination_comment(self): np = self.nc.nominee_set.first().nomineeposition_set.first() hash = get_hash_nominee_position(np.time.strftime("%Y%m%d"),np.id) @@ -1224,4 +1256,188 @@ class NewActiveNomComTests(TestCase): response = self.client.post(url,{'comments':'A nonempty comment'}) self.assertEqual(response.status_code,200) self.assertEqual(Feedback.objects.count(),feedback_count_before+1) + + def test_provide_private_key(self): + url = reverse('nomcom_private_key',kwargs={'year':self.nc.year()}) + login_testing_unauthorized(self,self.chair.user.username,url) + response = self.client.get(url) + self.assertEqual(response.status_code,200) + response = self.client.post(url,{'key':key}) + self.assertEqual(response.status_code,302) + + def test_email_pasting(self): + url = reverse('nomcom_private_feedback_email',kwargs={'year':self.nc.year()}) + login_testing_unauthorized(self,self.chair.user.username,url) + response = self.client.get(url) + self.assertTrue(response.status_code,200) + fb_count_before = Feedback.objects.count() + response = self.client.post(url,{'email_text':"""To: rjsparks@nostrum.com +From: Robert Sparks +Subject: Junk message for feedback testing +Message-ID: <566F2FE5.1050401@nostrum.com> +Date: Mon, 14 Dec 2015 15:08:53 -0600 +Content-Type: text/plain; charset=utf-8; format=flowed +Content-Transfer-Encoding: 7bit + +Junk body for testing + +"""}) + self.assertEqual(response.status_code,200) + self.assertEqual(Feedback.objects.count(),fb_count_before+1) + + def test_simple_feedback_pending(self): + url = reverse('nomcom_view_feedback_pending',kwargs={'year':self.nc.year() }) + login_testing_unauthorized(self, self.chair.user.username, url) + provide_private_key_to_test_client(self) + + # test simple classification when there's only one thing to classify + + # junk is the only category you can set directly from the first form the view presents + fb = FeedbackFactory(nomcom=self.nc,type_id=None) + response = self.client.get(url) + self.assertEqual(response.status_code,200) + + response = self.client.post(url, {'form-TOTAL_FORMS': 1, + 'form-INITIAL_FORMS': 1, + 'form-0-id': fb.id, + 'form-0-type': 'junk', + }) + self.assertEqual(response.status_code,302) + fb = Feedback.objects.get(id=fb.id) + self.assertEqual(fb.type_id,'junk') + + # comments, nominations, and questionnare responses are catagorized via a second + # formset presented by the view (signaled by having 'end' appear in the POST) + fb = FeedbackFactory(nomcom=self.nc,type_id=None) + np = NomineePosition.objects.filter(position__nomcom = self.nc,state='accepted').first() + fb_count_before = np.nominee.feedback_set.count() + response = self.client.post(url, {'form-TOTAL_FORMS':1, + 'form-INITIAL_FORMS':1, + 'end':'Save feedback', + 'form-0-id': fb.id, + 'form-0-type': 'comment', + 'form-0-nominee': '%s_%s'%(np.position.id,np.nominee.id), + }) + self.assertEqual(response.status_code,302) + fb = Feedback.objects.get(id=fb.id) + self.assertEqual(fb.type_id,'comment') + self.assertEqual(np.nominee.feedback_set.count(),fb_count_before+1) + + fb = FeedbackFactory(nomcom=self.nc,type_id=None) + nominee = self.nc.nominee_set.first() + position = self.nc.position_set.exclude(nomineeposition__nominee=nominee).first() + self.assertIsNotNone(position) + fb_count_before = nominee.feedback_set.count() + response = self.client.post(url, {'form-TOTAL_FORMS':1, + 'form-INITIAL_FORMS':1, + 'end':'Save feedback', + 'form-0-id': fb.id, + 'form-0-type': 'nomina', + 'form-0-position': position.id, + 'form-0-candidate_name' : nominee.name(), + 'form-0-candidate_email' : nominee.email.address, + }) + self.assertEqual(response.status_code,302) + fb = Feedback.objects.get(id=fb.id) + self.assertEqual(fb.type_id,'nomina') + self.assertEqual(nominee.feedback_set.count(),fb_count_before+1) + + fb = FeedbackFactory(nomcom=self.nc,type_id=None) + np = NomineePosition.objects.filter(position__nomcom = self.nc,state='accepted').first() + fb_count_before = np.nominee.feedback_set.count() + response = self.client.post(url, {'form-TOTAL_FORMS':1, + 'form-INITIAL_FORMS':1, + 'end':'Save feedback', + 'form-0-id': fb.id, + 'form-0-type': 'questio', + 'form-0-nominee': '%s_%s'%(np.position.id,np.nominee.id), + }) + self.assertEqual(response.status_code,302) + fb = Feedback.objects.get(id=fb.id) + self.assertEqual(fb.type_id,'questio') + self.assertEqual(np.nominee.feedback_set.count(),fb_count_before+1) + + def test_complicated_feedback_pending(self): + url = reverse('nomcom_view_feedback_pending',kwargs={'year':self.nc.year() }) + login_testing_unauthorized(self, self.chair.user.username, url) + provide_private_key_to_test_client(self) + + # Test having multiple things to classify + # The view has some complicated to handle having some forms in the initial form formset + # being categorized as 'junk' and others being categorized as something that requires + # more information. The second formset presented will have forms for any others initially + # categorized as nominations, then a third formset will be presented with any that were + # initially categorized as comments or questionnaire responses. The following exercises + # all the gears that glue these three formset presentations together. + fb0 = FeedbackFactory(nomcom=self.nc,type_id=None) + fb1 = FeedbackFactory(nomcom=self.nc,type_id=None) + fb2 = FeedbackFactory(nomcom=self.nc,type_id=None) + nominee = self.nc.nominee_set.first() + new_position_for_nominee = self.nc.position_set.exclude(nomineeposition__nominee=nominee).first() + + # Initial formset + response = self.client.post(url, {'form-TOTAL_FORMS': 3, + 'form-INITIAL_FORMS': 3, + 'form-0-id': fb0.id, + 'form-0-type': 'junk', + 'form-1-id': fb1.id, + 'form-1-type': 'nomina', + 'form-2-id': fb2.id, + 'form-2-type': 'comment', + }) + self.assertEqual(response.status_code,200) # Notice that this is not a 302 + fb0 = Feedback.objects.get(id=fb0.id) + self.assertEqual(fb0.type_id,'junk') + q = PyQuery(response.content) + self.assertEqual(q('input[name=\"form-0-type\"]').attr['value'],'nomina') + self.assertEqual(q('input[name=\"extra_ids\"]').attr['value'],'%s:comment' % fb2.id) + + # Second formset + response = self.client.post(url, {'form-TOTAL_FORMS':1, + 'form-INITIAL_FORMS':1, + 'end':'Save feedback', + 'form-0-id': fb1.id, + 'form-0-type': 'nomina', + 'form-0-position': new_position_for_nominee.id, + 'form-0-candidate_name' : nominee.name(), + 'form-0-candidate_email' : nominee.email.address, + 'extra_ids': '%s:comment' % fb2.id, + }) + self.assertEqual(response.status_code,200) # Notice that this is also is not a 302 + fb1 = Feedback.objects.get(id=fb1.id) + self.assertEqual(fb1.type_id,'nomina') + q = PyQuery(response.content) + self.assertEqual(q('input[name=\"form-0-type\"]').attr['value'],'comment') + self.assertFalse(q('input[name=\"extra_ids\"]')) + + # Exercising the resulting third formset is identical to the simple test above + # that categorizes a single thing as a comment. Note that it returns a 302. + + # There is yet another code-path for transitioning to the second form when + # nothing was classified as a nomination. + fb0 = FeedbackFactory(nomcom=self.nc,type_id=None) + fb1 = FeedbackFactory(nomcom=self.nc,type_id=None) + np = NomineePosition.objects.filter(position__nomcom = self.nc,state='accepted').first() + response = self.client.post(url, {'form-TOTAL_FORMS': 2, + 'form-INITIAL_FORMS': 2, + 'form-0-id': fb0.id, + 'form-0-type': 'junk', + 'form-1-id': fb1.id, + 'form-1-type': 'comment', + }) + self.assertEqual(response.status_code,200) + q = PyQuery(response.content) + self.assertEqual(q('input[name=\"form-0-type\"]').attr['value'],'comment') + self.assertFalse(q('input[name=\"extra_ids\"]')) + + +class NomComIndexTests(TestCase): + def setUp(self): + for year in range(2000,2014): + NomComFactory.create(**nomcom_kwargs_for_year(year=year,populate_positions=False,populate_personnel=False)) + + def testIndex(self): + url = reverse('ietf.nomcom.views.index') + response = self.client.get(url) + self.assertEqual(response.status_code,200) diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py index c24c56774..0440954de 100644 --- a/ietf/nomcom/views.py +++ b/ietf/nomcom/views.py @@ -533,7 +533,7 @@ def process_nomination_status(request, year, nominee_position_id, state, date, h expiration_days = getattr(settings, 'DAYS_TO_EXPIRE_NOMINATION_LINK', None) if expiration_days: request_date = datetime.date(int(date[:4]), int(date[4:6]), int(date[6:])) - if datetime.date.today() > (request_date + datetime.timedelta(days=settings.DAYS_TO_EXPIRE_REGISTRATION_LINK)): + if datetime.date.today() > (request_date + datetime.timedelta(days=settings.DAYS_TO_EXPIRE_NOMINATION_LINK)): return HttpResponseForbidden("Link expired") need_confirmation = True From f68d546233a5d9f2f61fd97dd3ad4888fcc7bec6 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 16 Dec 2015 12:07:32 +0000 Subject: [PATCH 26/37] removed move_to_default - Legacy-Id: 10603 --- ietf/nomcom/forms.py | 18 ------------------ ietf/nomcom/views.py | 18 +----------------- ietf/settings.py | 1 - .../nomcom/view_feedback_pending.html | 3 --- 4 files changed, 1 insertion(+), 39 deletions(-) diff --git a/ietf/nomcom/forms.py b/ietf/nomcom/forms.py index 854f22e3c..d7835ec6f 100644 --- a/ietf/nomcom/forms.py +++ b/ietf/nomcom/forms.py @@ -609,13 +609,6 @@ class PendingFeedbackForm(BaseNomcomForm, forms.ModelForm): model = Feedback fields = ('type', ) - def __init__(self, *args, **kwargs): - super(PendingFeedbackForm, self).__init__(*args, **kwargs) - try: - self.default_type = FeedbackTypeName.objects.get(slug=settings.DEFAULT_FEEDBACK_TYPE) - except FeedbackTypeName.DoesNotExist: - self.default_type = None - def set_nomcom(self, nomcom, user): self.nomcom = nomcom self.user = user @@ -631,17 +624,6 @@ class PendingFeedbackForm(BaseNomcomForm, forms.ModelForm): feedback.save() return feedback - def move_to_default(self): - if not self.default_type or self.cleaned_data.get('type', None): - return None - feedback = super(PendingFeedbackForm, self).save(commit=False) - feedback.nomcom = self.nomcom - feedback.user = self.user - feedback.type = self.default_type - feedback.save() - return feedback - - class ReminderDatesForm(forms.ModelForm): class Meta: diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py index 0440954de..518832c8e 100644 --- a/ietf/nomcom/views.py +++ b/ietf/nomcom/views.py @@ -656,24 +656,9 @@ def view_feedback_pending(request, year): extra=0) feedbacks = Feedback.objects.filter(type__isnull=True, nomcom=nomcom) - try: - default_type = FeedbackTypeName.objects.get(slug=settings.DEFAULT_FEEDBACK_TYPE) - except FeedbackTypeName.DoesNotExist: - default_type = None extra_step = False - if request.method == 'POST' and request.POST.get('move_to_default'): - formset = FeedbackFormSet(request.POST) - if formset.is_valid(): - for form in formset.forms: - form.set_nomcom(nomcom, request.user) - form.move_to_default() - formset = FeedbackFormSet(queryset=feedbacks) - for form in formset.forms: - form.set_nomcom(nomcom, request.user) - messages.success(request, 'Feedback saved') - return redirect('nomcom_view_feedback_pending', year=year) - elif request.method == 'POST' and request.POST.get('end'): + if request.method == 'POST' and request.POST.get('end'): extra_ids = request.POST.get('extra_ids', None) extra_step = True formset = FullFeedbackFormSet(request.POST) @@ -751,7 +736,6 @@ def view_feedback_pending(request, year): 'formset': formset, 'message': message, 'extra_step': extra_step, - 'default_type': default_type, 'type_dict': type_dict, 'extra_ids': extra_ids, 'types': FeedbackTypeName.objects.all().order_by('pk'), diff --git a/ietf/settings.py b/ietf/settings.py index 40aa1d207..f4ea228c3 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -455,7 +455,6 @@ NOMCOM_PUBLIC_KEYS_DIR = '/a/www/nomcom/public_keys/' NOMCOM_FROM_EMAIL = 'nomcom-chair@ietf.org' OPENSSL_COMMAND = '/usr/bin/openssl' DAYS_TO_EXPIRE_NOMINATION_LINK = '' -DEFAULT_FEEDBACK_TYPE = 'offtopic' NOMINEE_FEEDBACK_TYPES = ['comment', 'questio', 'nomina'] # ID Submission Tool settings diff --git a/ietf/templates/nomcom/view_feedback_pending.html b/ietf/templates/nomcom/view_feedback_pending.html index 7302b7008..d4be2c183 100644 --- a/ietf/templates/nomcom/view_feedback_pending.html +++ b/ietf/templates/nomcom/view_feedback_pending.html @@ -149,9 +149,6 @@ {% buttons %} - {% if default_type %} - - {% endif %} {% endbuttons %} {% endif %} From de0b7c9a378a89997b8f47b25840c7698ec56dc8 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 16 Dec 2015 23:12:40 +0000 Subject: [PATCH 27/37] Checkpoint: views tests at 96% - Legacy-Id: 10606 --- ietf/nomcom/tests.py | 120 +++++++++++++++++++++++++++++++++++++++++-- ietf/nomcom/views.py | 8 +-- 2 files changed, 119 insertions(+), 9 deletions(-) diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py index a22088e85..b8202039e 100644 --- a/ietf/nomcom/tests.py +++ b/ietf/nomcom/tests.py @@ -6,6 +6,7 @@ import shutil from pyquery import PyQuery from django.db import IntegrityError +from django.db.models import Max from django.conf import settings from django.core.urlresolvers import reverse from django.core.files import File @@ -35,6 +36,7 @@ from ietf.nomcom.factories import NomComFactory, FeedbackFactory, \ key from ietf.person.factories import PersonFactory from ietf.dbtemplate.factories import DBTemplateFactory +from ietf.dbtemplate.models import DBTemplate client_test_cert_files = None @@ -1104,7 +1106,7 @@ class InactiveNomcomTests(TestCase): title='Test template', path='/nomcom/'+self.nc.group.acronym+'/test', variables='', - type_id='text', + type_id='plain', content='test content') url = reverse('nomcom_edit_template',kwargs={'year':self.nc.year(), 'template_id':template.id}) login_testing_unauthorized(self, self.chair.user.username, url) @@ -1269,7 +1271,7 @@ class NewActiveNomComTests(TestCase): url = reverse('nomcom_private_feedback_email',kwargs={'year':self.nc.year()}) login_testing_unauthorized(self,self.chair.user.username,url) response = self.client.get(url) - self.assertTrue(response.status_code,200) + self.assertEqual(response.status_code,200) fb_count_before = Feedback.objects.count() response = self.client.post(url,{'email_text':"""To: rjsparks@nostrum.com From: Robert Sparks @@ -1418,7 +1420,6 @@ Junk body for testing # nothing was classified as a nomination. fb0 = FeedbackFactory(nomcom=self.nc,type_id=None) fb1 = FeedbackFactory(nomcom=self.nc,type_id=None) - np = NomineePosition.objects.filter(position__nomcom = self.nc,state='accepted').first() response = self.client.post(url, {'form-TOTAL_FORMS': 2, 'form-INITIAL_FORMS': 2, 'form-0-id': fb0.id, @@ -1431,6 +1432,90 @@ Junk body for testing self.assertEqual(q('input[name=\"form-0-type\"]').attr['value'],'comment') self.assertFalse(q('input[name=\"extra_ids\"]')) + def test_feedback_unrelated(self): + FeedbackFactory(nomcom=self.nc,type_id='junk') + url=reverse('nomcom_view_feedback_unrelated',kwargs={'year':self.nc.year()}) + login_testing_unauthorized(self,self.chair.user.username,url) + provide_private_key_to_test_client(self) + response = self.client.get(url) + self.assertEqual(response.status_code,200) + + def test_list_templates(self): + DBTemplateFactory.create(group=self.nc.group, + title='Test template', + path='/nomcom/'+self.nc.group.acronym+'/test', + variables='', + type_id='plain', + content='test content') + url=reverse('nomcom_list_templates',kwargs={'year':self.nc.year()}) + login_testing_unauthorized(self,self.chair.user.username,url) + response = self.client.get(url) + self.assertEqual(response.status_code,200) + + def test_edit_templates(self): + template = DBTemplateFactory.create(group=self.nc.group, + title='Test template', + path='/nomcom/'+self.nc.group.acronym+'/test', + variables='', + type_id='plain', + content='test content') + url=reverse('nomcom_edit_template',kwargs={'year':self.nc.year(),'template_id':template.id}) + login_testing_unauthorized(self,self.chair.user.username,url) + response = self.client.get(url) + self.assertEqual(response.status_code,200) + response = self.client.post(url,{'content': 'more interesting test content'}) + self.assertEqual(response.status_code,302) + template = DBTemplate.objects.get(id=template.id) + self.assertEqual('more interesting test content',template.content) + + def test_list_positions(self): + url = reverse('nomcom_list_positions',kwargs={'year':self.nc.year()}) + login_testing_unauthorized(self,self.chair.user.username,url) + response = self.client.get(url) + self.assertEqual(response.status_code,200) + + def test_remove_position(self): + position = self.nc.position_set.filter(nomineeposition__isnull=False).first() + f = FeedbackFactory(nomcom=self.nc) + f.positions.add(position) + url = reverse('nomcom_remove_position',kwargs={'year':self.nc.year(),'position_id':position.id}) + login_testing_unauthorized(self,self.chair.user.username,url) + response = self.client.get(url) + self.assertEqual(response.status_code,200) + q = PyQuery(response.content) + self.assertTrue(any(['likely to be harmful' in x.text for x in q('.alert-warning')])) + response = self.client.post(url,{'remove':position.id}) + self.assertEqual(response.status_code, 302) + self.assertFalse(self.nc.position_set.filter(id=position.id)) + + def test_remove_invalid_position(self): + no_such_position_id = self.nc.position_set.aggregate(Max('id'))['id__max']+1 + url = reverse('nomcom_remove_position',kwargs={'year':self.nc.year(),'position_id':no_such_position_id}) + login_testing_unauthorized(self,self.chair.user.username,url) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + def test_edit_position(self): + position = self.nc.position_set.filter(is_open=True).first() + url = reverse('nomcom_edit_position',kwargs={'year':self.nc.year(),'position_id':position.id}) + login_testing_unauthorized(self,self.chair.user.username,url) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + response = self.client.post(url,{'name':'more interesting test name'}) + self.assertEqual(response.status_code, 302) + position = Position.objects.get(id=position.id) + self.assertEqual('more interesting test name',position.name) + self.assertFalse(position.is_open) + + def test_edit_invalid_position(self): + no_such_position_id = self.nc.position_set.aggregate(Max('id'))['id__max']+1 + url = reverse('nomcom_edit_position',kwargs={'year':self.nc.year(),'position_id':no_such_position_id}) + login_testing_unauthorized(self,self.chair.user.username,url) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + # Note that the old tests currently test the edit_position view through its nomcom_add_position name + class NomComIndexTests(TestCase): def setUp(self): @@ -1441,3 +1526,32 @@ class NomComIndexTests(TestCase): url = reverse('ietf.nomcom.views.index') response = self.client.get(url) self.assertEqual(response.status_code,200) + +class NoPublicKeyTests(TestCase): + def setUp(self): + self.nc = NomComFactory.create(**nomcom_kwargs_for_year(public_key=None)) + self.chair = self.nc.group.role_set.filter(name='chair').first().person + + def do_common_work(self,url,expected_form): + login_testing_unauthorized(self,self.chair.user.username,url) + response = self.client.get(url) + self.assertEqual(response.status_code,200) + q=PyQuery(response.content) + self.assertTrue(any(['not yet' in x.text for x in q('.alert-warning')])) + self.assertEqual(bool(q('form:not(.navbar-form)')),expected_form) + self.client.logout() + + def test_not_yet(self): + # Warn reminder mail + self.do_common_work(reverse('nomcom_send_reminder_mail',kwargs={'year':self.nc.year(),'type':'accept'}),True) + # No nominations + self.do_common_work(reverse('nomcom_private_nominate',kwargs={'year':self.nc.year()}),False) + # No feedback + self.do_common_work(reverse('nomcom_private_feedback',kwargs={'year':self.nc.year()}),False) + # No feedback email + self.do_common_work(reverse('nomcom_private_feedback_email',kwargs={'year':self.nc.year()}),False) + # No questionnaire responses + self.do_common_work(reverse('nomcom_private_questionnaire',kwargs={'year':self.nc.year()}),False) + # Warn on edit nomcom + self.do_common_work(reverse('nomcom_edit_nomcom',kwargs={'year':self.nc.year()}),True) + diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py index 518832c8e..7e1325c48 100644 --- a/ietf/nomcom/views.py +++ b/ietf/nomcom/views.py @@ -38,11 +38,7 @@ import debug # pyflakes:ignore def index(request): nomcom_list = Group.objects.filter(type__slug='nomcom').order_by('acronym') for nomcom in nomcom_list: - year = nomcom.acronym[6:] - try: - year = int(year) - except ValueError: - year = None + year = int(nomcom.acronym[6:]) nomcom.year = year nomcom.label = "%s/%s" % (year, year+1) if year in [ 2005, 2006, 2007, 2008, 2009, 2010 ]: @@ -816,7 +812,7 @@ def edit_nomcom(request, year): if nomcom.public_key: message = ('warning', 'Previous data will remain encrypted with the old key') else: - message = ('warning', 'The nomcom has not a public key yet') + message = ('warning', 'This Nomcom does not yet have a public key') ReminderDateInlineFormSet = inlineformset_factory(parent_model=NomCom, model=ReminderDates, From ebb6884b70a9845231f919f2de220c0824870122 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 17 Dec 2015 21:53:39 +0000 Subject: [PATCH 28/37] Beginning to rework Nominee and Nomination - Legacy-Id: 10607 --- ietf/nomcom/migrations/0010_nominee_person.py | 27 +++++++++++++++++++ ietf/nomcom/models.py | 1 + 2 files changed, 28 insertions(+) create mode 100644 ietf/nomcom/migrations/0010_nominee_person.py diff --git a/ietf/nomcom/migrations/0010_nominee_person.py b/ietf/nomcom/migrations/0010_nominee_person.py new file mode 100644 index 000000000..3ff549479 --- /dev/null +++ b/ietf/nomcom/migrations/0010_nominee_person.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + +def populate_person(apps, schema_editor): + Nominee = apps.get_model('nomcom','Nominee') + for n in Nominee.objects.all(): + n.person = n.email.person + n.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('person', '0004_auto_20150308_0440'), + ('nomcom', '0009_remove_requirements_dbtemplate_type_from_path'), + ] + + operations = [ + migrations.AddField( + model_name='nominee', + name='person', + field=models.ForeignKey(blank=True, to='person.Person', null=True), + preserve_default=True, + ), + migrations.RunPython(populate_person,None) + ] diff --git a/ietf/nomcom/models.py b/ietf/nomcom/models.py index 238249ed8..ea6f24983 100644 --- a/ietf/nomcom/models.py +++ b/ietf/nomcom/models.py @@ -111,6 +111,7 @@ class Nomination(models.Model): class Nominee(models.Model): email = models.ForeignKey(Email) + person = models.ForeignKey(Person, blank=True, null=True) nominee_position = models.ManyToManyField('Position', through='NomineePosition') duplicated = models.ForeignKey('Nominee', blank=True, null=True) nomcom = models.ForeignKey('NomCom') From a2af7cfa256bcc3664aba1bc10b8d83941c487d1 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 17 Dec 2015 22:31:29 +0000 Subject: [PATCH 29/37] Removed BaseNomcomForm and the notion of custom fieldsets - Legacy-Id: 10608 --- ietf/nomcom/forms.py | 73 +++++---------------------- ietf/templates/nomcom/nomcomform.html | 33 ------------ 2 files changed, 12 insertions(+), 94 deletions(-) delete mode 100644 ietf/templates/nomcom/nomcomform.html diff --git a/ietf/nomcom/forms.py b/ietf/nomcom/forms.py index d7835ec6f..317a2d3fd 100644 --- a/ietf/nomcom/forms.py +++ b/ietf/nomcom/forms.py @@ -88,34 +88,10 @@ class MultiplePositionNomineeField(forms.MultipleChoiceField, PositionNomineeFie return result -class BaseNomcomForm(object): - def __unicode__(self): - return self.as_div() - - def as_div(self): - return render_to_string('nomcom/nomcomform.html', {'form': self}) - - def get_fieldsets(self): - if not self.fieldsets: - yield dict(name=None, fields=self) - else: - for fieldset, fields in self.fieldsets: - fieldset_dict = dict(name=fieldset, fields=[]) - for field_name in fields: - if field_name in self.fields: - fieldset_dict['fields'].append(self[field_name]) - if not fieldset_dict['fields']: - # if there is no fields in this fieldset, we continue to next fieldset - continue - yield fieldset_dict - -class EditMembersForm(BaseNomcomForm, forms.Form): +class EditMembersForm(forms.Form): members = MultiEmailField(label="Members email", required=False, widget=forms.Textarea) - fieldsets = [('Members', ('members',))] - - class EditMembersFormPreview(FormPreview): form_template = 'nomcom/edit_members.html' preview_template = 'nomcom/edit_members_preview.html' @@ -212,10 +188,8 @@ class EditMembersFormPreview(FormPreview): return redirect('nomcom_edit_members', year=self.year) -class EditNomcomForm(BaseNomcomForm, forms.ModelForm): +class EditNomcomForm(forms.ModelForm): - fieldsets = [('Edit nomcom settings', ('public_key', 'initial_text', - 'send_questionnaire', 'reminder_interval'))] def __init__(self, *args, **kwargs): super(EditNomcomForm, self).__init__(*args, **kwargs) @@ -242,15 +216,13 @@ class EditNomcomForm(BaseNomcomForm, forms.ModelForm): raise forms.ValidationError('Invalid public key. Error was: %s' % error) -class MergeForm(BaseNomcomForm, forms.Form): +class MergeForm(forms.Form): secondary_emails = MultiEmailField(label="Secondary email addresses", help_text="Provide a comma separated list of email addresses. Nominations already received with any of these email address will be moved to show under the primary address.", widget=forms.Textarea) primary_email = forms.EmailField(label="Primary email address", widget=forms.TextInput(attrs={'size': '40'})) - fieldsets = [('Emails', ('primary_email', 'secondary_emails'))] - def __init__(self, *args, **kwargs): self.nomcom = kwargs.pop('nomcom', None) super(MergeForm, self).__init__(*args, **kwargs) @@ -326,16 +298,13 @@ class MergeForm(BaseNomcomForm, forms.Form): secondary_nominees.update(duplicated=primary_nominee) -class NominateForm(BaseNomcomForm, forms.ModelForm): +class NominateForm(forms.ModelForm): comments = forms.CharField(label="Candidate's qualifications for the position", widget=forms.Textarea()) confirmation = forms.BooleanField(label='Email comments back to me as confirmation.', help_text="If you want to get a confirmation mail containing your feedback in cleartext, please check the 'email comments back to me as confirmation'.", required=False) - fieldsets = [('Candidate Nomination', ('share_nominator','position', 'candidate_name', - 'candidate_email', 'candidate_phone', 'comments', 'confirmation'))] - def __init__(self, *args, **kwargs): self.nomcom = kwargs.pop('nomcom', None) self.user = kwargs.pop('user', None) @@ -343,19 +312,12 @@ class NominateForm(BaseNomcomForm, forms.ModelForm): super(NominateForm, self).__init__(*args, **kwargs) - fieldset = ['share_nominator', - 'position', - 'candidate_name', - 'candidate_email', 'candidate_phone', - 'comments'] - self.fields['nominator_email'].label = 'Nominator email' if self.nomcom: self.fields['position'].queryset = Position.objects.get_by_nomcom(self.nomcom).opened() self.fields['comments'].help_text = self.nomcom.initial_text if not self.public: - fieldset = ['nominator_email'] + fieldset author = get_user_email(self.user) if author: self.fields['nominator_email'].initial = author.address @@ -367,9 +329,8 @@ class NominateForm(BaseNomcomForm, forms.ModelForm): has indicated they will allow NomCom to share their name as one of the people nominating this candidate.""" else: - fieldset.append('confirmation') + pass - self.fieldsets = [('Candidate Nomination', fieldset)] def save(self, commit=True): # Create nomination @@ -433,7 +394,7 @@ class NominateForm(BaseNomcomForm, forms.ModelForm): 'candidate_email', 'candidate_phone') -class FeedbackForm(BaseNomcomForm, forms.ModelForm): +class FeedbackForm(forms.ModelForm): nominator_email = forms.CharField(label='Commenter email',required=False) comments = forms.CharField(label='Comments', @@ -516,12 +477,10 @@ class FeedbackForm(BaseNomcomForm, forms.ModelForm): 'confirmation', ) -class FeedbackEmailForm(BaseNomcomForm, forms.Form): +class FeedbackEmailForm(forms.Form): email_text = forms.CharField(label='Email text', widget=forms.Textarea()) - fieldsets = [('Feedback email', ('email_text',))] - def __init__(self, *args, **kwargs): self.nomcom = kwargs.pop('nomcom', None) super(FeedbackEmailForm, self).__init__(*args, **kwargs) @@ -529,12 +488,10 @@ class FeedbackEmailForm(BaseNomcomForm, forms.Form): def save(self, commit=True): create_feedback_email(self.nomcom, self.cleaned_data['email_text']) -class QuestionnaireForm(BaseNomcomForm, forms.ModelForm): +class QuestionnaireForm(forms.ModelForm): comments = forms.CharField(label='Questionnaire response from this candidate', widget=forms.Textarea()) - fieldsets = [('New questionnaire response', ('nominee', 'comments'))] - def __init__(self, *args, **kwargs): self.nomcom = kwargs.pop('nomcom', None) self.user = kwargs.pop('user', None) @@ -563,14 +520,10 @@ class QuestionnaireForm(BaseNomcomForm, forms.ModelForm): model = Feedback fields = ( 'comments', ) -class NomComTemplateForm(BaseNomcomForm, DBTemplateForm): +class NomComTemplateForm(DBTemplateForm): content = forms.CharField(label="Text", widget=forms.Textarea(attrs={'cols': '120', 'rows':'40', })) - fieldsets = [('Template content', ('content', )), ] - -class PositionForm(BaseNomcomForm, forms.ModelForm): - - fieldsets = [('Position', ('name', 'is_open' ))] +class PositionForm(forms.ModelForm): class Meta: model = Position @@ -585,12 +538,10 @@ class PositionForm(BaseNomcomForm, forms.ModelForm): super(PositionForm, self).save(*args, **kwargs) -class PrivateKeyForm(BaseNomcomForm, forms.Form): +class PrivateKeyForm(forms.Form): key = forms.CharField(label='Private key', widget=forms.Textarea(), required=False) - fieldsets = [('Private key', ('key',))] - def clean_key(self): key = self.cleaned_data.get('key', None) if not key: @@ -601,7 +552,7 @@ class PrivateKeyForm(BaseNomcomForm, forms.Form): raise forms.ValidationError('Invalid private key. Error was: %s' % error) -class PendingFeedbackForm(BaseNomcomForm, forms.ModelForm): +class PendingFeedbackForm(forms.ModelForm): type = forms.ModelChoiceField(queryset=FeedbackTypeName.objects.all().order_by('pk'), widget=forms.RadioSelect, empty_label='Unclassified', required=False) diff --git a/ietf/templates/nomcom/nomcomform.html b/ietf/templates/nomcom/nomcomform.html deleted file mode 100644 index 844d18028..000000000 --- a/ietf/templates/nomcom/nomcomform.html +++ /dev/null @@ -1,33 +0,0 @@ -{# Copyright The IETF Trust 2015, All Rights Reserved #}{% load origin %}{% origin %} -
    - {% for fieldset in form.get_fieldsets %} - {% if fieldset.name %} -
    -

    {{ fieldset.name }}

    - {% endif %} - - {% for field in fieldset.fields %} -
    - -
    -
    {{ field.help_text }}
    - {{ field }} -
    {{ field.errors }}
    -
    -
    -
    - {% endfor %} - - {% if fieldset.name %} -
    - {% endif %} - {% endfor %} - -
    - From 6bf422797440813b823d6f4eacd84d60a420317b Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Fri, 18 Dec 2015 20:38:17 +0000 Subject: [PATCH 30/37] Checkpoint: main nomination forms use SearchableEmailField - Legacy-Id: 10609 --- ietf/nomcom/factories.py | 4 +- ietf/nomcom/forms.py | 39 +++--- ietf/nomcom/tests.py | 124 ++++++++++++++++---- ietf/nomcom/utils.py | 64 +++++----- ietf/person/factories.py | 2 + ietf/person/models.py | 5 +- ietf/templates/nomcom/private_nominate.html | 11 ++ ietf/templates/nomcom/public_nominate.html | 11 ++ 8 files changed, 190 insertions(+), 70 deletions(-) diff --git a/ietf/nomcom/factories.py b/ietf/nomcom/factories.py index 049e32f62..c2059acfa 100644 --- a/ietf/nomcom/factories.py +++ b/ietf/nomcom/factories.py @@ -92,7 +92,7 @@ class NomComFactory(factory.DjangoModelFactory): if extracted is None: extracted = True if create and extracted: - nominees = [Nominee.objects.create(nomcom=self, email=PersonFactory().email_set.first()) for i in range(4)] + nominees = [NomineeFactory(nomcom=self) for i in range(4)] positions = [PositionFactory(nomcom=self) for i in range(3)] def npc(position,nominee,state_id): @@ -134,6 +134,8 @@ class NomineeFactory(factory.DjangoModelFactory): model = Nominee nomcom = factory.SubFactory(NomComFactory) + person = factory.SubFactory(PersonFactory) + email = factory.LazyAttribute(lambda n: n.person.email()) class FeedbackFactory(factory.DjangoModelFactory): class Meta: diff --git a/ietf/nomcom/forms.py b/ietf/nomcom/forms.py index 317a2d3fd..c52542aab 100644 --- a/ietf/nomcom/forms.py +++ b/ietf/nomcom/forms.py @@ -2,7 +2,6 @@ from django.conf import settings from django import forms from django.contrib.formtools.preview import FormPreview, AUTO_ID from django.shortcuts import get_object_or_404, redirect -from django.template.loader import render_to_string from django.utils.decorators import method_decorator from django.shortcuts import render_to_response from django.template.context import RequestContext @@ -15,8 +14,10 @@ from ietf.nomcom.models import ( NomCom, Nomination, Nominee, NomineePosition, Position, Feedback, ReminderDates ) from ietf.nomcom.utils import (NOMINATION_RECEIPT_TEMPLATE, FEEDBACK_RECEIPT_TEMPLATE, get_user_email, validate_private_key, validate_public_key, - get_or_create_nominee, create_feedback_email) + get_or_create_nominee, get_or_create_nominee_by_person, + create_feedback_email) from ietf.person.models import Email +from ietf.person.fields import SearchableEmailField from ietf.utils.fields import MultiEmailField from ietf.utils.mail import send_mail from ietf.mailtrigger.utils import gather_address_lists @@ -299,7 +300,8 @@ class MergeForm(forms.Form): class NominateForm(forms.ModelForm): - comments = forms.CharField(label="Candidate's qualifications for the position", + candidate = SearchableEmailField(only_users=False) + qualifications = forms.CharField(label="Candidate's qualifications for the position", widget=forms.Textarea()) confirmation = forms.BooleanField(label='Email comments back to me as confirmation.', help_text="If you want to get a confirmation mail containing your feedback in cleartext, please check the 'email comments back to me as confirmation'.", @@ -315,9 +317,10 @@ class NominateForm(forms.ModelForm): self.fields['nominator_email'].label = 'Nominator email' if self.nomcom: self.fields['position'].queryset = Position.objects.get_by_nomcom(self.nomcom).opened() - self.fields['comments'].help_text = self.nomcom.initial_text + self.fields['qualifications'].help_text = self.nomcom.initial_text if not self.public: + self.fields.pop('confirmation') author = get_user_email(self.user) if author: self.fields['nominator_email'].initial = author.address @@ -329,21 +332,26 @@ class NominateForm(forms.ModelForm): has indicated they will allow NomCom to share their name as one of the people nominating this candidate.""" else: - pass + self.fields.pop('nominator_email') def save(self, commit=True): # Create nomination nomination = super(NominateForm, self).save(commit=False) nominator_email = self.cleaned_data.get('nominator_email', None) - candidate_email = self.cleaned_data['candidate_email'] - candidate_name = self.cleaned_data['candidate_name'] + ## TODO - rename this candidate_email after purging the old candidate_email + candidate = self.cleaned_data['candidate'] + ##candidate_email = self.cleaned_data['candidate_email'] + ##candidate_name = self.cleaned_data['candidate_name'] position = self.cleaned_data['position'] - comments = self.cleaned_data['comments'] - confirmation = self.cleaned_data['confirmation'] + qualifications = self.cleaned_data['qualifications'] + confirmation = self.cleaned_data.get('confirmation', False) share_nominator = self.cleaned_data['share_nominator'] nomcom_template_path = '/nomcom/%s/' % self.nomcom.group.acronym + nomination.candidate_name = candidate.person.plain_name() + nomination.candidate_email = candidate.address + author = None if self.public: author = get_user_email(self.user) @@ -351,11 +359,12 @@ class NominateForm(forms.ModelForm): if nominator_email: emails = Email.objects.filter(address=nominator_email) author = emails and emails[0] or None - nominee = get_or_create_nominee(self.nomcom, candidate_name, candidate_email, position, author) + ##nominee = get_or_create_nominee(self.nomcom, candidate_name, candidate_email, position, author) + nominee = get_or_create_nominee_by_person (self.nomcom, candidate.person, position, author) # Complete nomination data feedback = Feedback.objects.create(nomcom=self.nomcom, - comments=comments, + comments=qualifications, type=FeedbackTypeName.objects.get(slug='nomina'), user=self.user) feedback.positions.add(position) @@ -381,7 +390,7 @@ class NominateForm(forms.ModelForm): from_email = settings.NOMCOM_FROM_EMAIL (to_email, cc) = gather_address_lists('nomination_receipt_requested',nominator=author.address) context = {'nominee': nominee.email.person.name, - 'comments': comments, + 'comments': qualifications, 'position': position.name} path = nomcom_template_path + NOMINATION_RECEIPT_TEMPLATE send_mail(None, to_email, from_email, subject, path, context, cc=cc) @@ -390,8 +399,10 @@ class NominateForm(forms.ModelForm): class Meta: model = Nomination - fields = ('share_nominator', 'position', 'nominator_email', 'candidate_name', - 'candidate_email', 'candidate_phone') + fields = ('share_nominator', 'position', 'nominator_email', 'candidate', + 'candidate_phone', 'qualifications', 'confirmation') + ##fields = ('share_nominator', 'position', 'nominator_email', 'candidate', 'candidate_name', + ## 'candidate_email', 'candidate_phone', 'qualifications', 'confirmation') class FeedbackForm(forms.ModelForm): diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py index b8202039e..ffb664954 100644 --- a/ietf/nomcom/tests.py +++ b/ietf/nomcom/tests.py @@ -34,7 +34,7 @@ from ietf.nomcom.management.commands.send_reminders import Command, is_time_to_s from ietf.nomcom.factories import NomComFactory, FeedbackFactory, \ nomcom_kwargs_for_year, provide_private_key_to_test_client, \ key -from ietf.person.factories import PersonFactory +from ietf.person.factories import PersonFactory, EmailFactory from ietf.dbtemplate.factories import DBTemplateFactory from ietf.dbtemplate.models import DBTemplate @@ -481,11 +481,7 @@ class NomcomViewsTest(TestCase): self.nominate_view(public=True,confirmation=True) - self.assertEqual(len(outbox), messages_before + 4) - - self.assertTrue('New person' in outbox[-4]['Subject']) - self.assertTrue('nomcomchair' in outbox[-4]['To']) - self.assertTrue('secretariat' in outbox[-4]['To']) + self.assertEqual(len(outbox), messages_before + 3) self.assertEqual('IETF Nomination Information', outbox[-3]['Subject']) self.assertTrue('nominee' in outbox[-3]['To']) @@ -518,15 +514,23 @@ class NomcomViewsTest(TestCase): login_testing_unauthorized(self, COMMUNITY_USER, self.public_nominate_url) empty_outbox() self.nominate_view(public=True) - self.assertEqual(len(outbox), 4) + self.assertEqual(len(outbox), 3) # test_public_nominate checks the other messages - self.assertTrue('Questionnaire' in outbox[2]['Subject']) - self.assertTrue('nominee@' in outbox[2]['To']) + self.assertTrue('Questionnaire' in outbox[1]['Subject']) + self.assertTrue('nominee@' in outbox[1]['To']) def nominate_view(self, *args, **kwargs): public = kwargs.pop('public', True) + nominee = kwargs.pop('nominee', None) nominee_email = kwargs.pop('nominee_email', u'nominee@example.com') + if not nominee: + nominee = Email.objects.filter(address=nominee_email).first() + if not nominee: + nominee = EmailFactory(address=nominee_email,primary=True) + if not nominee.person: + nominee.person = PersonFactory() + nominee.save() nominator_email = kwargs.pop('nominator_email', "%s%s" % (COMMUNITY_USER, EMAIL_DOMAIN)) position_name = kwargs.pop('position', 'IAOC') confirmation = kwargs.pop('confirmation', False) @@ -553,16 +557,13 @@ class NomcomViewsTest(TestCase): self.assertEqual(len(q("#nominate-form")), 1) position = Position.objects.get(name=position_name) - candidate_email = nominee_email - candidate_name = u'nominee' comments = u'Test nominate view. Comments with accents äöåÄÖÅ éáíóú âêîôû ü àèìòù.' candidate_phone = u'123456' - test_data = {'candidate_name': candidate_name, - 'candidate_email': candidate_email, + test_data = {'candidate': nominee.pk, 'candidate_phone': candidate_phone, 'position': position.id, - 'comments': comments, + 'qualifications': comments, 'confirmation': confirmation} if not public: test_data['nominator_email'] = nominator_email @@ -573,12 +574,11 @@ class NomcomViewsTest(TestCase): self.assertContains(response, "alert-success") # check objects - email = Email.objects.get(address=candidate_email) - Person.objects.get(name=candidate_name, address=candidate_email) - nominee = Nominee.objects.get(email=email) - NomineePosition.objects.get(position=position, nominee=nominee) + ## TODO - straighten this _obj vs _email naming mess out + nominee_obj = Nominee.objects.get(email=nominee) + NomineePosition.objects.get(position=position, nominee=nominee_obj) feedback = Feedback.objects.filter(positions__in=[position], - nominees__in=[nominee], + nominees__in=[nominee_obj], type=FeedbackTypeName.objects.get(slug='nomina')).latest('id') if public: self.assertEqual(feedback.author, nominator_email) @@ -588,13 +588,85 @@ class NomcomViewsTest(TestCase): self.assertEqual(check_comments(feedback.comments, comments, self.privatekey_file), True) Nomination.objects.get(position=position, - candidate_name=candidate_name, - candidate_email=candidate_email, + candidate_name=nominee.person.plain_name(), + candidate_email=nominee.address, candidate_phone=candidate_phone, - nominee=nominee, + nominee=nominee_obj, comments=feedback, nominator_email="%s%s" % (COMMUNITY_USER, EMAIL_DOMAIN)) +## Save this for repurposing to test the to-be-created 'by name and address' form +## def nominate_view(self, *args, **kwargs): +## public = kwargs.pop('public', True) +## nominee_email = kwargs.pop('nominee_email', u'nominee@example.com') +## nominator_email = kwargs.pop('nominator_email', "%s%s" % (COMMUNITY_USER, EMAIL_DOMAIN)) +## position_name = kwargs.pop('position', 'IAOC') +## confirmation = kwargs.pop('confirmation', False) +## +## if public: +## nominate_url = self.public_nominate_url +## else: +## nominate_url = self.private_nominate_url +## response = self.client.get(nominate_url) +## self.assertEqual(response.status_code, 200) +## +## nomcom = get_nomcom_by_year(self.year) +## if not nomcom.public_key: +## q = PyQuery(response.content) +## self.assertEqual(len(q("#nominate-form")), 0) +## +## # save the cert file in tmp +## nomcom.public_key.storage.location = tempfile.gettempdir() +## nomcom.public_key.save('cert', File(open(self.cert_file.name, 'r'))) +## +## response = self.client.get(nominate_url) +## self.assertEqual(response.status_code, 200) +## q = PyQuery(response.content) +## self.assertEqual(len(q("#nominate-form")), 1) +## +## position = Position.objects.get(name=position_name) +## candidate_email = nominee_email +## candidate_name = u'nominee' +## comments = u'Test nominate view. Comments with accents äöåÄÖÅ éáíóú âêîôû ü àèìòù.' +## candidate_phone = u'123456' +## +## test_data = {'candidate_name': candidate_name, +## 'candidate_email': candidate_email, +## 'candidate_phone': candidate_phone, +## 'position': position.id, +## 'qualifications': comments, +## 'confirmation': confirmation} +## if not public: +## test_data['nominator_email'] = nominator_email +## +## response = self.client.post(nominate_url, test_data) +## self.assertEqual(response.status_code, 200) +## q = PyQuery(response.content) +## self.assertContains(response, "alert-success") +## +## # check objects +## email = Email.objects.get(address=candidate_email) +## Person.objects.get(name=candidate_name, address=candidate_email) +## nominee = Nominee.objects.get(email=email) +## NomineePosition.objects.get(position=position, nominee=nominee) +## feedback = Feedback.objects.filter(positions__in=[position], +## nominees__in=[nominee], +## type=FeedbackTypeName.objects.get(slug='nomina')).latest('id') +## if public: +## self.assertEqual(feedback.author, nominator_email) +## +## # to check feedback comments are saved like enrypted data +## self.assertNotEqual(feedback.comments, comments) +## +## self.assertEqual(check_comments(feedback.comments, comments, self.privatekey_file), True) +## Nomination.objects.get(position=position, +## candidate_name=candidate_name, +## candidate_email=candidate_email, +## candidate_phone=candidate_phone, +## nominee=nominee, +## comments=feedback, +## nominator_email="%s%s" % (COMMUNITY_USER, EMAIL_DOMAIN)) +## def test_add_questionnaire(self): self.access_chair_url(self.add_questionnaire_url) return self.add_questionnaire() @@ -656,10 +728,10 @@ class NomcomViewsTest(TestCase): self.feedback_view(public=True,confirmation=True) # feedback_view does a nomination internally: there is a lot of email related to that - tested elsewhere # We're interested in the confirmation receipt here - self.assertEqual(len(outbox),4) - self.assertEqual('NomCom comment confirmation', outbox[3]['Subject']) - self.assertTrue('plain' in outbox[3]['To']) - self.assertTrue(u'Comments with accents äöå' in unicode(outbox[3].get_payload(decode=True),"utf-8","replace")) + self.assertEqual(len(outbox),3) + self.assertEqual('NomCom comment confirmation', outbox[2]['Subject']) + self.assertTrue('plain' in outbox[2]['To']) + self.assertTrue(u'Comments with accents äöå' in unicode(outbox[2].get_payload(decode=True),"utf-8","replace")) empty_outbox() self.feedback_view(public=True) diff --git a/ietf/nomcom/utils.py b/ietf/nomcom/utils.py index 1349c175e..a2a231bd3 100644 --- a/ietf/nomcom/utils.py +++ b/ietf/nomcom/utils.py @@ -271,42 +271,22 @@ def send_reminder_to_nominees(nominees,type): return addrs -def get_or_create_nominee(nomcom, candidate_name, candidate_email, position, author): +def get_or_create_nominee_by_person(nomcom, candidate, position, author): from ietf.nomcom.models import Nominee, NomineePosition nomcom_template_path = '/nomcom/%s/' % nomcom.group.acronym - # Create person and email if candidate email does't exist and send email - email, created_email = Email.objects.get_or_create(address=candidate_email) - if created_email: - person = Person.objects.create(name=candidate_name, - ascii=unaccent.asciify(candidate_name), - address=candidate_email) - email.person = person - email.save() - # Add the nomination for a particular position - nominee, created = Nominee.objects.get_or_create(email=email, nomcom=nomcom) + nominee, created = Nominee.objects.get_or_create(person=candidate,email=candidate.email(), nomcom=nomcom) while nominee.duplicated: nominee = nominee.duplicated nominee_position, nominee_position_created = NomineePosition.objects.get_or_create(position=position, nominee=nominee) - if created_email: - # send email to secretariat and nomcomchair to warn about the new person - subject = 'New person is created' - from_email = settings.NOMCOM_FROM_EMAIL - (to_email, cc) = gather_address_lists('nomination_created_person',nomcom=nomcom) - context = {'email': email.address, - 'fullname': email.person.name, - 'person_id': email.person.id} - path = nomcom_template_path + INEXISTENT_PERSON_TEMPLATE - send_mail(None, to_email, from_email, subject, path, context, cc=cc) - if nominee_position_created: # send email to nominee subject = 'IETF Nomination Information' from_email = settings.NOMCOM_FROM_EMAIL - (to_email, cc) = gather_address_lists('nomination_new_nominee',nominee=email.address) + (to_email, cc) = gather_address_lists('nomination_new_nominee',nominee=nominee.email.address) domain = Site.objects.get_current().domain today = datetime.date.today().strftime('%Y%m%d') hash = get_hash_nominee_position(today, nominee_position.id) @@ -325,7 +305,7 @@ def get_or_create_nominee(nomcom, candidate_name, candidate_email, position, aut today, hash)) - context = {'nominee': email.person.name, + context = {'nominee': nominee.person.name, 'position': position.name, 'domain': domain, 'accept_url': accept_url, @@ -338,8 +318,8 @@ def get_or_create_nominee(nomcom, candidate_name, candidate_email, position, aut if nomcom.send_questionnaire: subject = '%s Questionnaire' % position from_email = settings.NOMCOM_FROM_EMAIL - (to_email, cc) = gather_address_lists('nomcom_questionnaire',nominee=email.address) - context = {'nominee': email.person.name, + (to_email, cc) = gather_address_lists('nomcom_questionnaire',nominee=nominee.email.address) + context = {'nominee': nominee.person.name, 'position': position.name} path = '%s%d/%s' % (nomcom_template_path, position.id, HEADER_QUESTIONNAIRE_TEMPLATE) @@ -353,8 +333,8 @@ def get_or_create_nominee(nomcom, candidate_name, candidate_email, position, aut subject = 'Nomination Information' from_email = settings.NOMCOM_FROM_EMAIL (to_email, cc) = gather_address_lists('nomination_received',nomcom=nomcom) - context = {'nominee': email.person.name, - 'nominee_email': email.address, + context = {'nominee': nominee.person.name, + 'nominee_email': nominee.email.address, 'position': position.name} if author: @@ -369,6 +349,34 @@ def get_or_create_nominee(nomcom, candidate_name, candidate_email, position, aut return nominee +def get_or_create_nominee(nomcom, candidate_name, candidate_email, position, author): + + ## TODO: Assert here that there is no matching email or person, and change the code + ## to not possibly stomp on existing things + + # Create person and email if candidate email does't exist and send email + email, created_email = Email.objects.get_or_create(address=candidate_email) + if created_email: + person = Person.objects.create(name=candidate_name, + ascii=unaccent.asciify(candidate_name), + address=candidate_email) + email.person = person + email.save() + + if created_email: + # send email to secretariat and nomcomchair to warn about the new person + subject = 'New person is created' + from_email = settings.NOMCOM_FROM_EMAIL + (to_email, cc) = gather_address_lists('nomination_created_person',nomcom=nomcom) + context = {'email': email.address, + 'fullname': email.person.name, + 'person_id': email.person.id} + nomcom_template_path = '/nomcom/%s/' % nomcom.group.acronym + path = nomcom_template_path + INEXISTENT_PERSON_TEMPLATE + send_mail(None, to_email, from_email, subject, path, context, cc=cc) + + return get_or_create_nominee_by_person(nomcom, email.person, position, author) + def getheader(header_text, default="ascii"): """Decode the specified header""" diff --git a/ietf/person/factories.py b/ietf/person/factories.py index 79db6fcff..f638a3d48 100644 --- a/ietf/person/factories.py +++ b/ietf/person/factories.py @@ -54,3 +54,5 @@ class EmailFactory(factory.DjangoModelFactory): django_get_or_create = ('address',) address = '%s.%s@%s' % (factory.Faker('first_name'),factory.Faker('last_name'),factory.Faker('domain_name')) + active = True + primary = False diff --git a/ietf/person/models.py b/ietf/person/models.py index e47276da0..db4f0f679 100644 --- a/ietf/person/models.py +++ b/ietf/person/models.py @@ -59,10 +59,13 @@ class PersonInfo(models.Model): if e: return e[0] return None - def email_address(self): + def email(self): e = self.email_set.filter(primary=True).first() if not e: e = self.email_set.filter(active=True).order_by("-time").first() + return e + def email_address(self): + e = self.email() if e: return e.address else: diff --git a/ietf/templates/nomcom/private_nominate.html b/ietf/templates/nomcom/private_nominate.html index 978946f57..c52044c5e 100644 --- a/ietf/templates/nomcom/private_nominate.html +++ b/ietf/templates/nomcom/private_nominate.html @@ -1,10 +1,16 @@ {% extends "nomcom/nomcom_private_base.html" %} {# Copyright The IETF Trust 2015, All Rights Reserved #} {% load origin %} +{% load staticfiles %} {% load bootstrap3 %} {% load nomcom_tags %} +{% block pagehead %} + + +{% endblock %} + {% block subtitle %} - Nominate{% endblock %} {% block nomcom_content %} @@ -30,3 +36,8 @@ {% endif %} {% endblock %} + +{% block js %} + + +{% endblock %} diff --git a/ietf/templates/nomcom/public_nominate.html b/ietf/templates/nomcom/public_nominate.html index 2e9fa842b..7bc7c211e 100644 --- a/ietf/templates/nomcom/public_nominate.html +++ b/ietf/templates/nomcom/public_nominate.html @@ -1,10 +1,16 @@ {% extends "nomcom/nomcom_public_base.html" %} {# Copyright The IETF Trust 2015, All Rights Reserved #} {% load origin %} +{% load staticfiles %} {% load bootstrap3 %} {% load nomcom_tags %} +{% block pagehead %} + + +{% endblock %} + {% block subtitle %} - Nominate{% endblock %} {% block nomcom_content %} @@ -26,3 +32,8 @@ {% endif %} {% endblock %} + +{% block js %} + + +{% endblock %} From 8261507e802142dd1b556326a8eac5ef76421c4d Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Fri, 18 Dec 2015 23:11:24 +0000 Subject: [PATCH 31/37] Added forms for the exceptional "create a new person for this nomination" workflow. Checkpointing before a round of simplification refactors. - Legacy-Id: 10610 --- ietf/nomcom/forms.py | 133 ++++++++++++++++++++++++--- ietf/nomcom/tests.py | 210 +++++++++++++++++++++++++------------------ ietf/nomcom/urls.py | 2 + ietf/nomcom/utils.py | 1 - ietf/nomcom/views.py | 54 ++++++++++- 5 files changed, 299 insertions(+), 101 deletions(-) diff --git a/ietf/nomcom/forms.py b/ietf/nomcom/forms.py index c52542aab..ee4107f6e 100644 --- a/ietf/nomcom/forms.py +++ b/ietf/nomcom/forms.py @@ -5,6 +5,8 @@ from django.shortcuts import get_object_or_404, redirect from django.utils.decorators import method_decorator from django.shortcuts import render_to_response from django.template.context import RequestContext +from django.core.urlresolvers import reverse +from django.utils.html import mark_safe from ietf.dbtemplate.forms import DBTemplateForm from ietf.group.models import Group, Role @@ -300,7 +302,7 @@ class MergeForm(forms.Form): class NominateForm(forms.ModelForm): - candidate = SearchableEmailField(only_users=False) + searched_email = SearchableEmailField(only_users=False) qualifications = forms.CharField(label="Candidate's qualifications for the position", widget=forms.Textarea()) confirmation = forms.BooleanField(label='Email comments back to me as confirmation.', @@ -314,6 +316,9 @@ class NominateForm(forms.ModelForm): super(NominateForm, self).__init__(*args, **kwargs) + new_person_url_name = 'nomcom_%s_nominate_newperson' % ('public' if self.public else 'private' ) + self.fields['searched_email'].label = 'Candidate email' + self.fields['searched_email'].help_text = 'Search by name or email address. Click here 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' if self.nomcom: self.fields['position'].queryset = Position.objects.get_by_nomcom(self.nomcom).opened() @@ -339,18 +344,15 @@ class NominateForm(forms.ModelForm): # Create nomination nomination = super(NominateForm, self).save(commit=False) nominator_email = self.cleaned_data.get('nominator_email', None) - ## TODO - rename this candidate_email after purging the old candidate_email - candidate = self.cleaned_data['candidate'] - ##candidate_email = self.cleaned_data['candidate_email'] - ##candidate_name = self.cleaned_data['candidate_name'] + searched_email = self.cleaned_data['searched_email'] position = self.cleaned_data['position'] qualifications = self.cleaned_data['qualifications'] confirmation = self.cleaned_data.get('confirmation', False) share_nominator = self.cleaned_data['share_nominator'] nomcom_template_path = '/nomcom/%s/' % self.nomcom.group.acronym - nomination.candidate_name = candidate.person.plain_name() - nomination.candidate_email = candidate.address + nomination.candidate_name = searched_email.person.plain_name() + nomination.candidate_email = searched_email.address author = None if self.public: @@ -359,8 +361,7 @@ class NominateForm(forms.ModelForm): if nominator_email: emails = Email.objects.filter(address=nominator_email) author = emails and emails[0] or None - ##nominee = get_or_create_nominee(self.nomcom, candidate_name, candidate_email, position, author) - nominee = get_or_create_nominee_by_person (self.nomcom, candidate.person, position, author) + nominee = get_or_create_nominee_by_person (self.nomcom, searched_email.person, position, author) # Complete nomination data feedback = Feedback.objects.create(nomcom=self.nomcom, @@ -399,10 +400,118 @@ class NominateForm(forms.ModelForm): class Meta: model = Nomination - fields = ('share_nominator', 'position', 'nominator_email', 'candidate', + fields = ('share_nominator', 'position', 'nominator_email', 'searched_email', 'candidate_phone', 'qualifications', 'confirmation') - ##fields = ('share_nominator', 'position', 'nominator_email', 'candidate', 'candidate_name', - ## 'candidate_email', 'candidate_phone', 'qualifications', 'confirmation') + +class NominateNewPersonForm(forms.ModelForm): + qualifications = forms.CharField(label="Candidate's qualifications for the position", + widget=forms.Textarea()) + confirmation = forms.BooleanField(label='Email comments back to me as confirmation.', + help_text="If you want to get a confirmation mail containing your feedback in cleartext, please check the 'email comments back to me as confirmation'.", + required=False) + + def __init__(self, *args, **kwargs): + self.nomcom = kwargs.pop('nomcom', None) + self.user = kwargs.pop('user', None) + self.public = kwargs.pop('public', None) + + super(NominateNewPersonForm, self).__init__(*args, **kwargs) + + self.fields['nominator_email'].label = 'Nominator email' + if self.nomcom: + self.fields['position'].queryset = Position.objects.get_by_nomcom(self.nomcom).opened() + self.fields['qualifications'].help_text = self.nomcom.initial_text + + if not self.public: + self.fields.pop('confirmation') + author = get_user_email(self.user) + if author: + self.fields['nominator_email'].initial = author.address + help_text = """(Nomcom Chair/Member: please fill this in. Use your own email address if the person making the + nomination wishes to be anonymous. The confirmation email will be sent to the address given here, + and the address will also be captured as part of the registered nomination.)""" + self.fields['nominator_email'].help_text = help_text + self.fields['share_nominator'].help_text = """(Nomcom Chair/Member: Check this box if the person providing this nomination + has indicated they will allow NomCom to share their name as one of the people + nominating this candidate.""" + else: + self.fields.pop('nominator_email') + + + def clean_candidate_email(self): + candidate_email = self.cleaned_data['candidate_email'] + if Email.objects.filter(address=candidate_email).exists(): + normal_url_name = 'nomcom_%s_nominate' % 'public' if self.public else 'private' + msg = '%s is already in the datatracker. \ + Use the normal nomination form to nominate the person \ + with this address.\ + ' % (candidate_email,reverse(normal_url_name,kwargs={'year':self.nomcom.year()})) + raise forms.ValidationError(mark_safe(msg)) + return candidate_email + + def save(self, commit=True): + # Create nomination + nomination = super(NominateNewPersonForm, self).save(commit=False) + nominator_email = self.cleaned_data.get('nominator_email', None) + candidate_email = self.cleaned_data['candidate_email'] + candidate_name = self.cleaned_data['candidate_name'] + position = self.cleaned_data['position'] + qualifications = self.cleaned_data['qualifications'] + confirmation = self.cleaned_data.get('confirmation', False) + share_nominator = self.cleaned_data['share_nominator'] + nomcom_template_path = '/nomcom/%s/' % self.nomcom.group.acronym + + + author = None + if self.public: + author = get_user_email(self.user) + else: + if nominator_email: + emails = Email.objects.filter(address=nominator_email) + author = emails and emails[0] or None + ## This is where it should change - validation of the email field should fail if the email exists + ## The function should become make_nominee_from_newperson) + nominee = get_or_create_nominee(self.nomcom, candidate_name, candidate_email, position, author) + + # Complete nomination data + feedback = Feedback.objects.create(nomcom=self.nomcom, + comments=qualifications, + type=FeedbackTypeName.objects.get(slug='nomina'), + user=self.user) + feedback.positions.add(position) + feedback.nominees.add(nominee) + + if author: + nomination.nominator_email = author.address + feedback.author = author.address + feedback.save() + + nomination.nominee = nominee + nomination.comments = feedback + nomination.share_nominator = share_nominator + nomination.user = self.user + + if commit: + nomination.save() + + # send receipt email to nominator + if confirmation: + if author: + subject = 'Nomination receipt' + from_email = settings.NOMCOM_FROM_EMAIL + (to_email, cc) = gather_address_lists('nomination_receipt_requested',nominator=author.address) + context = {'nominee': nominee.email.person.name, + 'comments': qualifications, + 'position': position.name} + path = nomcom_template_path + NOMINATION_RECEIPT_TEMPLATE + send_mail(None, to_email, from_email, subject, path, context, cc=cc) + + return nomination + + class Meta: + model = Nomination + fields = ('share_nominator', 'position', 'nominator_email', 'candidate_name', + 'candidate_email', 'candidate_phone', 'qualifications', 'confirmation') class FeedbackForm(forms.ModelForm): diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py index ffb664954..6f55743f5 100644 --- a/ietf/nomcom/tests.py +++ b/ietf/nomcom/tests.py @@ -75,6 +75,7 @@ class NomcomViewsTest(TestCase): self.edit_members_url = reverse('nomcom_edit_members', kwargs={'year': self.year}) self.edit_nomcom_url = reverse('nomcom_edit_nomcom', kwargs={'year': self.year}) self.private_nominate_url = reverse('nomcom_private_nominate', kwargs={'year': self.year}) + self.private_nominate_newperson_url = reverse('nomcom_private_nominate_newperson', kwargs={'year': self.year}) self.add_questionnaire_url = reverse('nomcom_private_questionnaire', kwargs={'year': self.year}) self.private_feedback_url = reverse('nomcom_private_feedback', kwargs={'year': self.year}) self.positions_url = reverse("nomcom_list_positions", kwargs={'year': self.year}) @@ -86,6 +87,7 @@ class NomcomViewsTest(TestCase): self.questionnaires_url = reverse('nomcom_questionnaires', kwargs={'year': self.year}) self.public_feedback_url = reverse('nomcom_public_feedback', kwargs={'year': self.year}) self.public_nominate_url = reverse('nomcom_public_nominate', kwargs={'year': self.year}) + self.public_nominate_newperson_url = reverse('nomcom_public_nominate_newperson', kwargs={'year': self.year}) def tearDown(self): clean_test_public_keys_dir(self) @@ -507,6 +509,42 @@ class NomcomViewsTest(TestCase): return self.nominate_view(public=False) self.client.logout() + def test_public_nominate_newperson(self): + login_testing_unauthorized(self, COMMUNITY_USER, self.public_nominate_url) + + messages_before = len(outbox) + + self.nominate_newperson_view(public=True,confirmation=True) + + self.assertEqual(len(outbox), messages_before + 4) + + self.assertEqual('New person is created', outbox[-4]['Subject']) + self.assertTrue('secretariat' in outbox[-4]['To']) + + self.assertEqual('IETF Nomination Information', outbox[-3]['Subject']) + self.assertTrue('nominee' in outbox[-3]['To']) + + self.assertEqual('Nomination Information', outbox[-2]['Subject']) + self.assertTrue('nomcomchair' in outbox[-2]['To']) + + self.assertEqual('Nomination receipt', outbox[-1]['Subject']) + self.assertTrue('plain' in outbox[-1]['To']) + self.assertTrue(u'Comments with accents äöå' in unicode(outbox[-1].get_payload(decode=True),"utf-8","replace")) + + # Nominate the same person for the same position again without asking for confirmation + + messages_before = len(outbox) + + self.nominate_view(public=True) + self.assertEqual(len(outbox), messages_before + 1) + self.assertEqual('Nomination Information', outbox[-1]['Subject']) + self.assertTrue('nomcomchair' in outbox[-1]['To']) + + def test_private_nominate_newperson(self): + self.access_member_url(self.private_nominate_url) + return self.nominate_newperson_view(public=False) + self.client.logout() + def test_public_nominate_with_automatic_questionnaire(self): nomcom = get_nomcom_by_year(self.year) nomcom.send_questionnaire = True @@ -522,15 +560,15 @@ class NomcomViewsTest(TestCase): def nominate_view(self, *args, **kwargs): public = kwargs.pop('public', True) - nominee = kwargs.pop('nominee', None) + searched_email = kwargs.pop('searched_email', None) nominee_email = kwargs.pop('nominee_email', u'nominee@example.com') - if not nominee: - nominee = Email.objects.filter(address=nominee_email).first() - if not nominee: - nominee = EmailFactory(address=nominee_email,primary=True) - if not nominee.person: - nominee.person = PersonFactory() - nominee.save() + if not searched_email: + searched_email = Email.objects.filter(address=nominee_email).first() + if not searched_email: + searched_email = EmailFactory(address=nominee_email,primary=True) + if not searched_email.person: + searched_email.person = PersonFactory() + searched_email.save() nominator_email = kwargs.pop('nominator_email', "%s%s" % (COMMUNITY_USER, EMAIL_DOMAIN)) position_name = kwargs.pop('position', 'IAOC') confirmation = kwargs.pop('confirmation', False) @@ -560,7 +598,7 @@ class NomcomViewsTest(TestCase): comments = u'Test nominate view. Comments with accents äöåÄÖÅ éáíóú âêîôû ü àèìòù.' candidate_phone = u'123456' - test_data = {'candidate': nominee.pk, + test_data = {'searched_email': searched_email.pk, 'candidate_phone': candidate_phone, 'position': position.id, 'qualifications': comments, @@ -574,11 +612,10 @@ class NomcomViewsTest(TestCase): self.assertContains(response, "alert-success") # check objects - ## TODO - straighten this _obj vs _email naming mess out - nominee_obj = Nominee.objects.get(email=nominee) - NomineePosition.objects.get(position=position, nominee=nominee_obj) + nominee = Nominee.objects.get(email=searched_email) + NomineePosition.objects.get(position=position, nominee=nominee) feedback = Feedback.objects.filter(positions__in=[position], - nominees__in=[nominee_obj], + nominees__in=[nominee], type=FeedbackTypeName.objects.get(slug='nomina')).latest('id') if public: self.assertEqual(feedback.author, nominator_email) @@ -589,84 +626,83 @@ class NomcomViewsTest(TestCase): self.assertEqual(check_comments(feedback.comments, comments, self.privatekey_file), True) Nomination.objects.get(position=position, candidate_name=nominee.person.plain_name(), - candidate_email=nominee.address, + candidate_email=searched_email.address, candidate_phone=candidate_phone, - nominee=nominee_obj, + nominee=nominee, + comments=feedback, + nominator_email="%s%s" % (COMMUNITY_USER, EMAIL_DOMAIN)) + + def nominate_newperson_view(self, *args, **kwargs): + public = kwargs.pop('public', True) + nominee_email = kwargs.pop('nominee_email', u'nominee@example.com') + nominator_email = kwargs.pop('nominator_email', "%s%s" % (COMMUNITY_USER, EMAIL_DOMAIN)) + position_name = kwargs.pop('position', 'IAOC') + confirmation = kwargs.pop('confirmation', False) + + if public: + nominate_url = self.public_nominate_newperson_url + else: + nominate_url = self.private_nominate_newperson_url + response = self.client.get(nominate_url) + self.assertEqual(response.status_code, 200) + + nomcom = get_nomcom_by_year(self.year) + if not nomcom.public_key: + q = PyQuery(response.content) + self.assertEqual(len(q("#nominate-form")), 0) + + # save the cert file in tmp + nomcom.public_key.storage.location = tempfile.gettempdir() + nomcom.public_key.save('cert', File(open(self.cert_file.name, 'r'))) + + response = self.client.get(nominate_url) + self.assertEqual(response.status_code, 200) + q = PyQuery(response.content) + self.assertEqual(len(q("#nominate-form")), 1) + + position = Position.objects.get(name=position_name) + candidate_email = nominee_email + candidate_name = u'nominee' + comments = u'Test nominate view. Comments with accents äöåÄÖÅ éáíóú âêîôû ü àèìòù.' + candidate_phone = u'123456' + + test_data = {'candidate_name': candidate_name, + 'candidate_email': candidate_email, + 'candidate_phone': candidate_phone, + 'position': position.id, + 'qualifications': comments, + 'confirmation': confirmation} + if not public: + test_data['nominator_email'] = nominator_email + + response = self.client.post(nominate_url, test_data) + self.assertEqual(response.status_code, 200) + q = PyQuery(response.content) + self.assertContains(response, "alert-success") + + # check objects + email = Email.objects.get(address=candidate_email) + Person.objects.get(name=candidate_name, address=candidate_email) + nominee = Nominee.objects.get(email=email) + NomineePosition.objects.get(position=position, nominee=nominee) + feedback = Feedback.objects.filter(positions__in=[position], + nominees__in=[nominee], + type=FeedbackTypeName.objects.get(slug='nomina')).latest('id') + if public: + self.assertEqual(feedback.author, nominator_email) + + # to check feedback comments are saved like enrypted data + self.assertNotEqual(feedback.comments, comments) + + self.assertEqual(check_comments(feedback.comments, comments, self.privatekey_file), True) + Nomination.objects.get(position=position, + candidate_name=candidate_name, + candidate_email=candidate_email, + candidate_phone=candidate_phone, + nominee=nominee, comments=feedback, nominator_email="%s%s" % (COMMUNITY_USER, EMAIL_DOMAIN)) -## Save this for repurposing to test the to-be-created 'by name and address' form -## def nominate_view(self, *args, **kwargs): -## public = kwargs.pop('public', True) -## nominee_email = kwargs.pop('nominee_email', u'nominee@example.com') -## nominator_email = kwargs.pop('nominator_email', "%s%s" % (COMMUNITY_USER, EMAIL_DOMAIN)) -## position_name = kwargs.pop('position', 'IAOC') -## confirmation = kwargs.pop('confirmation', False) -## -## if public: -## nominate_url = self.public_nominate_url -## else: -## nominate_url = self.private_nominate_url -## response = self.client.get(nominate_url) -## self.assertEqual(response.status_code, 200) -## -## nomcom = get_nomcom_by_year(self.year) -## if not nomcom.public_key: -## q = PyQuery(response.content) -## self.assertEqual(len(q("#nominate-form")), 0) -## -## # save the cert file in tmp -## nomcom.public_key.storage.location = tempfile.gettempdir() -## nomcom.public_key.save('cert', File(open(self.cert_file.name, 'r'))) -## -## response = self.client.get(nominate_url) -## self.assertEqual(response.status_code, 200) -## q = PyQuery(response.content) -## self.assertEqual(len(q("#nominate-form")), 1) -## -## position = Position.objects.get(name=position_name) -## candidate_email = nominee_email -## candidate_name = u'nominee' -## comments = u'Test nominate view. Comments with accents äöåÄÖÅ éáíóú âêîôû ü àèìòù.' -## candidate_phone = u'123456' -## -## test_data = {'candidate_name': candidate_name, -## 'candidate_email': candidate_email, -## 'candidate_phone': candidate_phone, -## 'position': position.id, -## 'qualifications': comments, -## 'confirmation': confirmation} -## if not public: -## test_data['nominator_email'] = nominator_email -## -## response = self.client.post(nominate_url, test_data) -## self.assertEqual(response.status_code, 200) -## q = PyQuery(response.content) -## self.assertContains(response, "alert-success") -## -## # check objects -## email = Email.objects.get(address=candidate_email) -## Person.objects.get(name=candidate_name, address=candidate_email) -## nominee = Nominee.objects.get(email=email) -## NomineePosition.objects.get(position=position, nominee=nominee) -## feedback = Feedback.objects.filter(positions__in=[position], -## nominees__in=[nominee], -## type=FeedbackTypeName.objects.get(slug='nomina')).latest('id') -## if public: -## self.assertEqual(feedback.author, nominator_email) -## -## # to check feedback comments are saved like enrypted data -## self.assertNotEqual(feedback.comments, comments) -## -## self.assertEqual(check_comments(feedback.comments, comments, self.privatekey_file), True) -## Nomination.objects.get(position=position, -## candidate_name=candidate_name, -## candidate_email=candidate_email, -## candidate_phone=candidate_phone, -## nominee=nominee, -## comments=feedback, -## nominator_email="%s%s" % (COMMUNITY_USER, EMAIL_DOMAIN)) -## def test_add_questionnaire(self): self.access_chair_url(self.add_questionnaire_url) return self.add_questionnaire() diff --git a/ietf/nomcom/urls.py b/ietf/nomcom/urls.py index e213f8871..69473b229 100644 --- a/ietf/nomcom/urls.py +++ b/ietf/nomcom/urls.py @@ -9,6 +9,7 @@ urlpatterns = patterns('ietf.nomcom.views', url(r'^(?P\d{4})/private/key/$', 'private_key', name='nomcom_private_key'), url(r'^(?P\d{4})/private/help/$', 'configuration_help', name='nomcom_chair_help'), url(r'^(?P\d{4})/private/nominate/$', 'private_nominate', name='nomcom_private_nominate'), + url(r'^(?P\d{4})/private/nominate/newperson$', 'private_nominate_newperson', name='nomcom_private_nominate_newperson'), url(r'^(?P\d{4})/private/feedback/$', 'private_feedback', name='nomcom_private_feedback'), url(r'^(?P\d{4})/private/feedback-email/$', 'private_feedback_email', name='nomcom_private_feedback_email'), url(r'^(?P\d{4})/private/questionnaire-response/$', 'private_questionnaire', name='nomcom_private_questionnaire'), @@ -35,6 +36,7 @@ urlpatterns = patterns('ietf.nomcom.views', url(r'^(?P\d{4})/questionnaires/$', 'questionnaires', name='nomcom_questionnaires'), url(r'^(?P\d{4})/feedback/$', 'public_feedback', name='nomcom_public_feedback'), url(r'^(?P\d{4})/nominate/$', 'public_nominate', name='nomcom_public_nominate'), + url(r'^(?P\d{4})/nominate/newperson$', 'public_nominate_newperson', name='nomcom_public_nominate_newperson'), url(r'^(?P\d{4})/process-nomination-status/(?P\d+)/(?P[\w]+)/(?P[\d]+)/(?P[a-f0-9]+)/$', 'process_nomination_status', name='nomcom_process_nomination_status'), ) diff --git a/ietf/nomcom/utils.py b/ietf/nomcom/utils.py index a2a231bd3..a793193ea 100644 --- a/ietf/nomcom/utils.py +++ b/ietf/nomcom/utils.py @@ -363,7 +363,6 @@ def get_or_create_nominee(nomcom, candidate_name, candidate_email, position, aut email.person = person email.save() - if created_email: # send email to secretariat and nomcomchair to warn about the new person subject = 'New person is created' from_email = settings.NOMCOM_FROM_EMAIL diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py index 7e1325c48..db0102f87 100644 --- a/ietf/nomcom/views.py +++ b/ietf/nomcom/views.py @@ -22,7 +22,7 @@ from ietf.group.models import Group, GroupEvent from ietf.message.models import Message from ietf.nomcom.decorators import nomcom_private_key_required -from ietf.nomcom.forms import (NominateForm, FeedbackForm, QuestionnaireForm, +from ietf.nomcom.forms import (NominateForm, NominateNewPersonForm, FeedbackForm, QuestionnaireForm, MergeForm, NomComTemplateForm, PositionForm, PrivateKeyForm, EditNomcomForm, EditNomineeForm, PendingFeedbackForm, ReminderDatesForm, FullFeedbackFormSet, @@ -361,6 +361,58 @@ def nominate(request, year, public): 'year': year, 'selected': 'nominate'}, RequestContext(request)) +@login_required +def public_nominate_newperson(request, year): + return nominate_newperson(request, year, True) + + +@role_required("Nomcom") +def private_nominate_newperson(request, year): + return nominate_newperson(request, year, False) + + +def nominate_newperson(request, year, public): + nomcom = get_nomcom_by_year(year) + has_publickey = nomcom.public_key and True or False + if public: + template = 'nomcom/public_nominate.html' + else: + template = 'nomcom/private_nominate.html' + + if not has_publickey: + message = ('warning', "This Nomcom is not yet accepting nominations") + return render_to_response(template, + {'message': message, + 'nomcom': nomcom, + 'year': year, + 'selected': 'nominate'}, RequestContext(request)) + + if nomcom.group.state_id == 'conclude': + message = ('warning', "Nominations to this Nomcom are closed.") + return render_to_response(template, + {'message': message, + 'nomcom': nomcom, + 'year': year, + 'selected': 'nominate'}, RequestContext(request)) + + message = None + if request.method == 'POST': + form = NominateNewPersonForm(data=request.POST, nomcom=nomcom, user=request.user, public=public) + if form.is_valid(): + form.save() + message = ('success', 'Your nomination has been registered. Thank you for the nomination.') + ## This needs to redirect to the normal nominate url instead. + ## Need to weed out the custom message stuff + form = NominateNewPersonForm(nomcom=nomcom, user=request.user, public=public) + else: + form = NominateNewPersonForm(nomcom=nomcom, user=request.user, public=public) + + return render_to_response(template, + {'form': form, + 'message': message, + 'nomcom': nomcom, + 'year': year, + 'selected': 'nominate'}, RequestContext(request)) @login_required def public_feedback(request, year): From 2120b79d6b9b75db77b8c19f60c7c3e95c49ca1b Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Mon, 21 Dec 2015 16:50:46 +0000 Subject: [PATCH 32/37] collapse some duplicated code - Legacy-Id: 10617 --- ietf/nomcom/views.py | 71 ++++++++++++-------------------------------- 1 file changed, 19 insertions(+), 52 deletions(-) diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py index db0102f87..92e56e609 100644 --- a/ietf/nomcom/views.py +++ b/ietf/nomcom/views.py @@ -312,66 +312,24 @@ def questionnaires(request, year): @login_required def public_nominate(request, year): - return nominate(request, year, True) + return nominate(request=request, year=year, public=True, newperson=False) @role_required("Nomcom") def private_nominate(request, year): - return nominate(request, year, False) - - -def nominate(request, year, public): - nomcom = get_nomcom_by_year(year) - has_publickey = nomcom.public_key and True or False - if public: - template = 'nomcom/public_nominate.html' - else: - template = 'nomcom/private_nominate.html' - - if not has_publickey: - message = ('warning', "This Nomcom is not yet accepting nominations") - return render_to_response(template, - {'message': message, - 'nomcom': nomcom, - 'year': year, - 'selected': 'nominate'}, RequestContext(request)) - - if nomcom.group.state_id == 'conclude': - message = ('warning', "Nominations to this Nomcom are closed.") - return render_to_response(template, - {'message': message, - 'nomcom': nomcom, - 'year': year, - 'selected': 'nominate'}, RequestContext(request)) - - message = None - if request.method == 'POST': - form = NominateForm(data=request.POST, nomcom=nomcom, user=request.user, public=public) - if form.is_valid(): - form.save() - message = ('success', 'Your nomination has been registered. Thank you for the nomination.') - form = NominateForm(nomcom=nomcom, user=request.user, public=public) - else: - form = NominateForm(nomcom=nomcom, user=request.user, public=public) - - return render_to_response(template, - {'form': form, - 'message': message, - 'nomcom': nomcom, - 'year': year, - 'selected': 'nominate'}, RequestContext(request)) + return nominate(request=request, year=year, public=False, newperson=False) @login_required def public_nominate_newperson(request, year): - return nominate_newperson(request, year, True) + return nominate(request=request, year=year, public=True, newperson=True) @role_required("Nomcom") def private_nominate_newperson(request, year): - return nominate_newperson(request, year, False) + return nominate(request=request, year=year, public=False, newperson=True) -def nominate_newperson(request, year, public): +def nominate(request, year, public, newperson): nomcom = get_nomcom_by_year(year) has_publickey = nomcom.public_key and True or False if public: @@ -397,15 +355,24 @@ def nominate_newperson(request, year, public): message = None if request.method == 'POST': - form = NominateNewPersonForm(data=request.POST, nomcom=nomcom, user=request.user, public=public) + if newperson: + form = NominateNewPersonForm(data=request.POST, nomcom=nomcom, user=request.user, public=public) + else: + form = NominateForm(data=request.POST, nomcom=nomcom, user=request.user, public=public) if form.is_valid(): form.save() message = ('success', 'Your nomination has been registered. Thank you for the nomination.') - ## This needs to redirect to the normal nominate url instead. - ## Need to weed out the custom message stuff - form = NominateNewPersonForm(nomcom=nomcom, user=request.user, public=public) + if newperson: + ## This needs to redirect to the normal nominate URL instead. + ## Need to weed out the custom message stuff. + form = NominateNewPersonForm(nomcom=nomcom, user=request.user, public=public) + else: + form = NominateForm(nomcom=nomcom, user=request.user, public=public) else: - form = NominateNewPersonForm(nomcom=nomcom, user=request.user, public=public) + if newperson: + form = NominateNewPersonForm(nomcom=nomcom, user=request.user, public=public) + else: + form = NominateForm(nomcom=nomcom, user=request.user, public=public) return render_to_response(template, {'form': form, From 85c13d65b4e58415c4a822e2341aeee46dfb63af Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Mon, 21 Dec 2015 20:46:56 +0000 Subject: [PATCH 33/37] Replaced the custom message infrastructure in the nomcom app with django.contrib.messages - Legacy-Id: 10618 --- ietf/nomcom/tests.py | 10 +- ietf/nomcom/views.py | 127 +++++++----------- ietf/templates/base.html | 11 +- ietf/templates/nomcom/chair_help.html | 3 - ietf/templates/nomcom/edit_members.html | 2 - ietf/templates/nomcom/edit_nomcom.html | 6 - ietf/templates/nomcom/edit_nominee.html | 5 - ietf/templates/nomcom/edit_position.html | 2 - ietf/templates/nomcom/feedback.html | 6 - .../nomcom/private_feedback_email.html | 5 - ietf/templates/nomcom/private_index.html | 3 - ietf/templates/nomcom/private_key.html | 4 - ietf/templates/nomcom/private_merge.html | 6 - ietf/templates/nomcom/private_nominate.html | 6 - .../nomcom/private_questionnaire.html | 6 - .../nomcom/process_nomination_status.html | 3 - ietf/templates/nomcom/public_nominate.html | 5 - ietf/templates/nomcom/send_reminder_mail.html | 6 - .../nomcom/view_feedback_pending.html | 3 - 19 files changed, 59 insertions(+), 160 deletions(-) diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py index 6f55743f5..c9dccd0cb 100644 --- a/ietf/nomcom/tests.py +++ b/ietf/nomcom/tests.py @@ -140,7 +140,7 @@ class NomcomViewsTest(TestCase): r = self.client.post(self.private_index_url, test_data) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertNotEqual(q('p.alert.alert-success'), []) + self.assertTrue(q('.alert-success')) self.assertEqual(NomineePosition.objects.filter(state='accepted').count (), 1) self.client.logout() @@ -152,7 +152,7 @@ class NomcomViewsTest(TestCase): r = self.client.post(self.private_index_url, test_data) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertNotEqual(q('p.alert.alert-success'), []) + self.assertTrue(q('.alert-success')) self.assertEqual(NomineePosition.objects.filter(state='declined').count (), 1) self.client.logout() @@ -164,7 +164,7 @@ class NomcomViewsTest(TestCase): r = self.client.post(self.private_index_url, test_data) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertNotEqual(q('p.alert.alert-success'), []) + self.assertTrue(q('.alert-success')) self.assertEqual(NomineePosition.objects.filter(state='pending').count (), 1) self.client.logout() @@ -1645,7 +1645,9 @@ class NoPublicKeyTests(TestCase): response = self.client.get(url) self.assertEqual(response.status_code,200) q=PyQuery(response.content) - self.assertTrue(any(['not yet' in x.text for x in q('.alert-warning')])) + text_bits = [x.xpath('./text()') for x in q('.alert-warning')] + flat_text_bits = [item for sublist in text_bits for item in sublist] + self.assertTrue(any(['not yet' in y for y in flat_text_bits])) self.assertEqual(bool(q('form:not(.navbar-form)')),expected_form) self.client.logout() diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py index 92e56e609..134f82ced 100644 --- a/ietf/nomcom/views.py +++ b/ietf/nomcom/views.py @@ -106,11 +106,11 @@ def announcements(request): @role_required("Nomcom") def private_key(request, year): nomcom = get_nomcom_by_year(year) - message = None + if request.session.get('NOMCOM_PRIVATE_KEY_%s' % year, None): - message = ('warning', 'You already have a private decryption key set for this session.') + messages.warning(request, 'You already have a private decryption key set for this session.') else: - message = ('warning', "You don't have a private decryption key set for this session yet") + messages.warning(request, "You don't have a private decryption key set for this session yet") back_url = request.GET.get('back_to', reverse('nomcom_private_index', None, args=(year, ))) if request.method == 'POST': @@ -125,7 +125,6 @@ def private_key(request, year): 'year': year, 'back_url': back_url, 'form': form, - 'message': message, 'selected': 'private_key'}, RequestContext(request)) @@ -134,10 +133,9 @@ def private_index(request, year): nomcom = get_nomcom_by_year(year) all_nominee_positions = NomineePosition.objects.get_by_nomcom(nomcom).not_duplicated() is_chair = nomcom.group.has_role(request.user, "chair") - message = None if is_chair and request.method == 'POST': if nomcom.group.state_id != 'active': - message = ('warning', "This nomcom is not active. Request administrative assistance if Nominee state needs to change.") + messages.warning(request, "This nomcom is not active. Request administrative assistance if Nominee state needs to change.") else: action = request.POST.get('action') nominations_to_modify = request.POST.getlist('selected') @@ -145,15 +143,15 @@ def private_index(request, year): nominations = all_nominee_positions.filter(id__in=nominations_to_modify) if action == "set_as_accepted": nominations.update(state='accepted') - message = ('success', 'The selected nominations have been set as accepted') + messages.success(request,'The selected nominations have been set as accepted') elif action == "set_as_declined": nominations.update(state='declined') - message = ('success', 'The selected nominations have been set as declined') + messages.success(request,'The selected nominations have been set as declined') elif action == "set_as_pending": nominations.update(state='pending') - message = ('success', 'The selected nominations have been set as pending') + messages.success(request,'The selected nominations have been set as pending') else: - message = ('warning', "Please, select some nominations to work with") + messages.warning(request, "Please, select some nominations to work with") filters = {} questionnaire_state = "questionnaire" @@ -195,7 +193,7 @@ def private_index(request, year): 'selected_position': selected_position and int(selected_position) or None, 'selected': 'index', 'is_chair': is_chair, - 'message': message}, RequestContext(request)) + }, RequestContext(request)) @role_required("Nomcom Chair", "Nomcom Advisor") @@ -205,11 +203,13 @@ def send_reminder_mail(request, year, type): has_publickey = nomcom.public_key and True or False if not has_publickey: - message = ('warning', "This Nomcom does not yet have a public key.") + messages.warning(request, "This Nomcom does not yet have a public key.") + nomcom_ready = False elif nomcom.group.state_id != 'active': - message = ('warning', "This Nomcom is not active.") + messages.warning(request, "This Nomcom is not active.") + nomcom_ready = False else: - message = None + nomcom_ready = True if type=='accept': interesting_state = 'pending' @@ -239,17 +239,17 @@ def send_reminder_mail(request, year, type): mail_template = DBTemplate.objects.filter(group=nomcom.group, path=mail_path) mail_template = mail_template and mail_template[0] or None - if request.method == 'POST' and not message: + if request.method == 'POST' and nomcom_ready: selected_nominees = request.POST.getlist('selected') selected_nominees = nominees.filter(id__in=selected_nominees) if selected_nominees: addrs = send_reminder_to_nominees(selected_nominees,type) if addrs: - message = ('success', 'A copy of "%s" has been sent to %s'%(mail_template.title,", ".join(addrs))) + messages.success(request, 'A copy of "%s" has been sent to %s'%(mail_template.title,", ".join(addrs))) else: - message = ('warning', 'No messages were sent.') + messages.warning(request, 'No messages were sent.') else: - message = ('warning', "Please, select at least one nominee") + messages.warning(request, "Please, select at least one nominee") return render_to_response('nomcom/send_reminder_mail.html', {'nomcom': nomcom, @@ -259,7 +259,6 @@ def send_reminder_mail(request, year, type): 'selected': selected_tab, 'reminder_description': reminder_description, 'state_description': state_description, - 'message': message, 'is_chair_task' : True, }, RequestContext(request)) @@ -268,15 +267,14 @@ def send_reminder_mail(request, year, type): def private_merge(request, year): nomcom = get_nomcom_by_year(year) if nomcom.group.state_id != 'active': - message = ('warning', "This Nomcom is not active.") + messages.warning(request, "This Nomcom is not active.") form = None else: - message = None if request.method == 'POST': form = MergeForm(request.POST, nomcom=nomcom) if form.is_valid(): form.save() - message = ('success', 'The emails have been unified') + messages.success(request, 'The emails have been unified') else: form = MergeForm(nomcom=nomcom) @@ -284,7 +282,6 @@ def private_merge(request, year): {'nomcom': nomcom, 'year': year, 'form': form, - 'message': message, 'selected': 'merge', 'is_chair_task' : True, }, RequestContext(request)) @@ -338,22 +335,19 @@ def nominate(request, year, public, newperson): template = 'nomcom/private_nominate.html' if not has_publickey: - message = ('warning', "This Nomcom is not yet accepting nominations") + messages.warning(request, "This Nomcom is not yet accepting nominations") return render_to_response(template, - {'message': message, - 'nomcom': nomcom, + {'nomcom': nomcom, 'year': year, 'selected': 'nominate'}, RequestContext(request)) if nomcom.group.state_id == 'conclude': - message = ('warning', "Nominations to this Nomcom are closed.") + messages.warning(request, "Nominations to this Nomcom are closed.") return render_to_response(template, - {'message': message, - 'nomcom': nomcom, + {'nomcom': nomcom, 'year': year, 'selected': 'nominate'}, RequestContext(request)) - message = None if request.method == 'POST': if newperson: form = NominateNewPersonForm(data=request.POST, nomcom=nomcom, user=request.user, public=public) @@ -361,7 +355,7 @@ def nominate(request, year, public, newperson): form = NominateForm(data=request.POST, nomcom=nomcom, user=request.user, public=public) if form.is_valid(): form.save() - message = ('success', 'Your nomination has been registered. Thank you for the nomination.') + messages.success(request, 'Your nomination has been registered. Thank you for the nomination.') if newperson: ## This needs to redirect to the normal nominate URL instead. ## Need to weed out the custom message stuff. @@ -376,7 +370,6 @@ def nominate(request, year, public, newperson): return render_to_response(template, {'form': form, - 'message': message, 'nomcom': nomcom, 'year': year, 'selected': 'nominate'}, RequestContext(request)) @@ -418,9 +411,8 @@ def feedback(request, year, public): base_template = "nomcom/nomcom_private_base.html" if not has_publickey: - message = ('warning', "This Nomcom is not yet accepting comments") + messages.warning(request, "This Nomcom is not yet accepting comments") return render(request, 'nomcom/feedback.html', { - 'message': message, 'nomcom': nomcom, 'year': year, 'selected': 'feedback', @@ -428,14 +420,13 @@ def feedback(request, year, public): 'base_template': base_template }) - message = None if nominee and position and request.method == 'POST': form = FeedbackForm(data=request.POST, nomcom=nomcom, user=request.user, public=public, position=position, nominee=nominee) if form.is_valid(): form.save() - message = ('success', 'Your feedback has been registered.') + messages.success(request, 'Your feedback has been registered.') form = None else: if nominee and position: @@ -446,7 +437,6 @@ def feedback(request, year, public): return render(request, 'nomcom/feedback.html', { 'form': form, - 'message': message, 'nomcom': nomcom, 'year': year, 'positions': positions, @@ -460,20 +450,20 @@ def feedback(request, year, public): def private_feedback_email(request, year): nomcom = get_nomcom_by_year(year) has_publickey = nomcom.public_key and True or False - message = None template = 'nomcom/private_feedback_email.html' if not has_publickey: - message = ('warning', "This Nomcom is not yet accepting feedback email.") + messages.warning(request, "This Nomcom is not yet accepting feedback email.") + nomcom_ready = False elif nomcom.group.state_id != 'active': - message = ('warning', "This Nomcom is not active, and is not accepting feedback email.") + messages.warning(request, "This Nomcom is not active, and is not accepting feedback email.") + nomcom_ready = False else: - pass + nomcom_ready = True - if message: + if not nomcom_ready: return render_to_response(template, - {'message': message, - 'nomcom': nomcom, + {'nomcom': nomcom, 'year': year, 'selected': 'feedback_email', 'is_chair_task' : True, @@ -487,35 +477,33 @@ def private_feedback_email(request, year): if form.is_valid(): form.save() form = FeedbackEmailForm(nomcom=nomcom) - message = ('success', 'The feedback email has been registered.') + messages.success(request, 'The feedback email has been registered.') return render_to_response(template, {'form': form, - 'message': message, 'nomcom': nomcom, 'year': year, 'selected': 'feedback_email'}, RequestContext(request)) - @role_required("Nomcom Chair", "Nomcom Advisor") def private_questionnaire(request, year): nomcom = get_nomcom_by_year(year) has_publickey = nomcom.public_key and True or False - message = None questionnaire_response = None template = 'nomcom/private_questionnaire.html' if not has_publickey: - message = ('warning', "This Nomcom is not yet accepting questionnaires.") + messages.warning(request, "This Nomcom is not yet accepting questionnaires.") + nomcom_ready = False elif nomcom.group.state_id != 'active': - message = ('warning', "This Nomcom is not active, and is not accepting questionnaires.") + messages.warning(request, "This Nomcom is not active, and is not accepting questionnaires.") + nomcom_ready = False else: - pass + nomcom_ready = True - if message: + if not nomcom_ready: return render_to_response(template, - {'message': message, - 'nomcom': nomcom, + {'nomcom': nomcom, 'year': year, 'selected': 'questionnaire', 'is_chair_task' : True, @@ -526,7 +514,7 @@ def private_questionnaire(request, year): nomcom=nomcom, user=request.user) if form.is_valid(): form.save() - message = ('success', 'The questionnaire response has been registered.') + messages.success(request, 'The questionnaire response has been registered.') questionnaire_response = form.cleaned_data['comments'] form = QuestionnaireForm(nomcom=nomcom, user=request.user) else: @@ -535,7 +523,6 @@ def private_questionnaire(request, year): return render_to_response(template, {'form': form, 'questionnaire_response': questionnaire_response, - 'message': message, 'nomcom': nomcom, 'year': year, 'selected': 'questionnaire'}, RequestContext(request)) @@ -560,10 +547,7 @@ def process_nomination_status(request, year, nominee_position_id, state, date, h return HttpResponseForbidden("The nomination already was %s" % nominee_position.state) state = get_object_or_404(NomineePositionStateName, slug=state) - message = ('warning', - ("Click on 'Save' to set the state of your nomination to %s to %s (this"+ - " is not a final commitment - you can notify us later if you need to change this).") % - (nominee_position.position.name, state.name)) + messages.info(request, "Click on 'Save' to set the state of your nomination to %s to %s (this is not a final commitment - you can notify us later if you need to change this)." % (nominee_position.position.name, state.name)) if request.method == 'POST': form = NominationResponseCommentForm(request.POST) if form.is_valid(): @@ -586,13 +570,11 @@ def process_nomination_status(request, year, nominee_position_id, state, date, h f.positions.add(nominee_position.position) f.nominees.add(nominee_position.nominee) - message = ('success', 'Your nomination on %s has been set as %s' % (nominee_position.position.name, - state.name)) + messages.success(request, 'Your nomination on %s has been set as %s' % (nominee_position.position.name, state.name)) else: form = NominationResponseCommentForm() return render_to_response('nomcom/process_nomination_status.html', - {'message': message, - 'nomcom': nomcom, + {'nomcom': nomcom, 'year': year, 'nominee_position': nominee_position, 'state': state, @@ -663,9 +645,6 @@ def view_feedback_pending(request, year): if nomcom.group.state_id == 'conclude': return HttpResponseForbidden("This nomcom is concluded.") extra_ids = None - message = None - for message in messages.get_messages(request): - message = ('success', message.message) FeedbackFormSet = modelformset_factory(Feedback, form=PendingFeedbackForm, extra=0) @@ -728,7 +707,7 @@ def view_feedback_pending(request, year): for form in formset.forms: form.set_nomcom(nomcom, request.user, extra) if moved: - message = ('success', '%s messages classified. You must enter more information for the following feedback.' % moved) + messages.success(request, '%s messages classified. You must enter more information for the following feedback.' % moved) else: messages.success(request, 'Feedback saved') return redirect('nomcom_view_feedback_pending', year=year) @@ -749,7 +728,6 @@ def view_feedback_pending(request, year): {'year': year, 'selected': 'feedback_pending', 'formset': formset, - 'message': message, 'extra_step': extra_step, 'type_dict': type_dict, 'extra_ids': extra_ids, @@ -802,14 +780,13 @@ def view_feedback_nominee(request, year, nominee_id): def edit_nominee(request, year, nominee_id): nomcom = get_nomcom_by_year(year) nominee = get_object_or_404(Nominee, id=nominee_id) - message = None if request.method == 'POST': form = EditNomineeForm(request.POST, instance=nominee) if form.is_valid(): form.save() - message = ('success', 'The nominee has been changed') + messages.success(request, 'The nominee has been changed') else: form = EditNomineeForm(instance=nominee) @@ -818,7 +795,6 @@ def edit_nominee(request, year, nominee_id): 'selected': 'index', 'nominee': nominee, 'form': form, - 'message': message, 'nomcom': nomcom, 'is_chair_task' : True, }, RequestContext(request)) @@ -829,9 +805,9 @@ def edit_nomcom(request, year): nomcom = get_nomcom_by_year(year) if nomcom.public_key: - message = ('warning', 'Previous data will remain encrypted with the old key') + messages.warning(request, 'Previous data will remain encrypted with the old key') else: - message = ('warning', 'This Nomcom does not yet have a public key') + messages.warning(request, 'This Nomcom does not yet have a public key') ReminderDateInlineFormSet = inlineformset_factory(parent_model=NomCom, model=ReminderDates, @@ -849,7 +825,7 @@ def edit_nomcom(request, year): form.save() formset.save() formset = ReminderDateInlineFormSet(instance=nomcom) - message = ('success', 'The nomcom has been changed') + messages.success(request, 'The nomcom has been changed') else: formset = ReminderDateInlineFormSet(instance=nomcom) form = EditNomcomForm(instance=nomcom) @@ -858,7 +834,6 @@ def edit_nomcom(request, year): {'form': form, 'formset': formset, 'nomcom': nomcom, - 'message': message, 'year': year, 'selected': 'edit_nomcom', 'is_chair_task' : True, diff --git a/ietf/templates/base.html b/ietf/templates/base.html index 12bf3097b..37122b359 100644 --- a/ietf/templates/base.html +++ b/ietf/templates/base.html @@ -1,6 +1,7 @@ {% load ietf_filters staticfiles %} {# Copyright The IETF Trust 2015, All Rights Reserved #} {% load origin %}{% origin %} +{% load bootstrap3 %} @@ -83,15 +84,7 @@ {% endwith %}
    - {% if messages %} -
    -
    - {% for message in messages %} -

    {{ message }}

    - {% endfor %} -
    -
    - {% endif %} + {% bootstrap_messages %} {% if request.COOKIES.left_menu != "off" and not hide_menu %} {# ugly hack for the more or less unported meeting agenda edit pages #}
    diff --git a/ietf/templates/nomcom/chair_help.html b/ietf/templates/nomcom/chair_help.html index aab6c6092..72845a84f 100644 --- a/ietf/templates/nomcom/chair_help.html +++ b/ietf/templates/nomcom/chair_help.html @@ -12,9 +12,6 @@ {% block nomcom_content %} {% origin %} - {% comment %} Why isn't this part of the base templates? {% endcomment %} - {% bootstrap_messages %} -