feat: replace references to User with references to Person (#6024)

* refactor: change references from User to Person (#5821)

* refactor: Change CommunityList reference from User to Person

* refactor: Convert more user references to person

* refactor: Change augment_docs_and_user_with_user_info to person

* refactor: Change Nomination and Feedback references from User to Person

* refactor: Change a few test case function signatures to be more pythonic

* refactor: Harmonize how profile and photo views look up email_or_name

* refactor: Rework community views to operate on Person instead of User (#5859)

* test: Update tests to try all of the person's emails and aliases

* fix: Recode a test case to avoid an exception if there's Unicode in the URL

This only happens using the form-filling and submission feature of
WebTest, which is only used in this one test case, so just it rip out.

* test: Add duplicate-person tests

* fix: If there are multiple matching users, prefer the logged-in one.

* chore: We no longer use WebTest, so don't include it.

* fix: Address review comments

* fix: case-insensitive person name or email matching (#6096)

* chore: Renumber migrations

* fix: Update merged code so tests pass (#6887)

* fix: Use refactored method

* fix: Don't assume user has person

* fix: Use new view param name

* chore: Drop community lists w/o person; cleanup (#6896)

* fix: Don't assume user has person

* fix: user->person in update_community_list_index.py

* feat: Remove CommunityLists without Person

* refactor: Speed up nomcom migrations

---------

Co-authored-by: Paul Selkirk <paul@painless-security.com>
Co-authored-by: Jennifer Richards <jennifer@staff.ietf.org>
This commit is contained in:
Robert Sparks 2024-01-24 11:00:19 -06:00 committed by GitHub
parent 36c43c8520
commit d9cc26be96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 691 additions and 459 deletions

View file

@ -89,7 +89,7 @@ class PersonalInformationExportView(DetailView, JsonExportMixin):
'sendqueue', 'nominee', 'topicfeedbacklastseen', 'alias', 'email', 'apikeys', 'personevent', 'sendqueue', 'nominee', 'topicfeedbacklastseen', 'alias', 'email', 'apikeys', 'personevent',
'reviewersettings', 'reviewsecretarysettings', 'unavailableperiod', 'reviewwish', 'reviewersettings', 'reviewsecretarysettings', 'unavailableperiod', 'reviewwish',
'nextreviewerinteam', 'reviewrequest', 'meetingregistration', 'submissionevent', 'preapproval', 'nextreviewerinteam', 'reviewrequest', 'meetingregistration', 'submissionevent', 'preapproval',
'user', 'user__communitylist', 'personextresource_set', ] 'user', 'communitylist', 'personextresource_set', ]
return self.json_view(request, filter={'id':person.id}, expand=expand) return self.json_view(request, filter={'id':person.id}, expand=expand)

View file

@ -7,8 +7,8 @@ from django.contrib import admin
from ietf.community.models import CommunityList, SearchRule, EmailSubscription from ietf.community.models import CommunityList, SearchRule, EmailSubscription
class CommunityListAdmin(admin.ModelAdmin): class CommunityListAdmin(admin.ModelAdmin):
list_display = ['id', 'user', 'group'] list_display = ['id', 'person', 'group']
raw_id_fields = ['user', 'group', 'added_docs'] raw_id_fields = ['person', 'group', 'added_docs']
admin.site.register(CommunityList, CommunityListAdmin) admin.site.register(CommunityList, CommunityListAdmin)
class SearchRuleAdmin(admin.ModelAdmin): class SearchRuleAdmin(admin.ModelAdmin):

View file

@ -114,14 +114,13 @@ class SearchRuleForm(forms.ModelForm):
class SubscriptionForm(forms.ModelForm): class SubscriptionForm(forms.ModelForm):
def __init__(self, user, clist, *args, **kwargs): def __init__(self, person, clist, *args, **kwargs):
self.clist = clist self.clist = clist
self.user = user
super(SubscriptionForm, self).__init__(*args, **kwargs) super(SubscriptionForm, self).__init__(*args, **kwargs)
self.fields["notify_on"].widget = forms.RadioSelect(choices=self.fields["notify_on"].choices) self.fields["notify_on"].widget = forms.RadioSelect(choices=self.fields["notify_on"].choices)
self.fields["email"].queryset = self.fields["email"].queryset.filter(person__user=user, active=True).order_by("-primary") self.fields["email"].queryset = self.fields["email"].queryset.filter(person=person, active=True).order_by("-primary")
self.fields["email"].widget = forms.RadioSelect(choices=[t for t in self.fields["email"].choices if t[0]]) self.fields["email"].widget = forms.RadioSelect(choices=[t for t in self.fields["email"].choices if t[0]])
if self.fields["email"].queryset: if self.fields["email"].queryset:

View file

@ -0,0 +1,26 @@
# Generated by Django 4.2.9 on 2024-01-05 21:28
from django.db import migrations
def forward(apps, schema_editor):
CommunityList = apps.get_model("community", "CommunityList")
# As of 2024-01-05, there are 570 personal CommunityLists with a user
# who has no associated Person. None of these has an EmailSubscription,
# so the lists are doing nothing and can be safely deleted.
personal_lists_no_person = CommunityList.objects.exclude(
user__isnull=True
).filter(
user__person__isnull=True
)
# Confirm the assumption that none of the lists to be deleted has an EmailSubscription
assert not personal_lists_no_person.filter(emailsubscription__isnull=False).exists()
personal_lists_no_person.delete()
class Migration(migrations.Migration):
dependencies = [
("community", "0003_track_rfcs"),
]
operations = [migrations.RunPython(forward)]

View file

@ -0,0 +1,54 @@
# Generated by Django 4.2.2 on 2023-06-12 19:35
from django.conf import settings
from django.db import migrations
import django.db.models.deletion
import ietf.utils.models
def forward(apps, schema_editor):
CommunityList = apps.get_model('community', 'CommunityList')
for clist in CommunityList.objects.all():
try:
clist.person = clist.user.person
except:
clist.person = None
clist.save()
def reverse(apps, schema_editor):
CommunityList = apps.get_model('community', 'CommunityList')
for clist in CommunityList.objects.all():
try:
clist.user = clist.person.user
except:
clist.user = None
clist.save()
class Migration(migrations.Migration):
dependencies = [
("community", "0004_delete_useless_community_lists"),
("person", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="communitylist",
name="person",
field=ietf.utils.models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="person.Person",
),
),
migrations.RunPython(forward, reverse),
migrations.RemoveField(
model_name="communitylist",
name="user",
field=ietf.utils.models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL),
),
]

View file

@ -2,7 +2,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.contrib.auth.models import User
from django.db import models from django.db import models
from django.db.models import signals from django.db.models import signals
from django.urls import reverse as urlreverse from django.urls import reverse as urlreverse
@ -13,13 +12,13 @@ from ietf.person.models import Person, Email
from ietf.utils.models import ForeignKey from ietf.utils.models import ForeignKey
class CommunityList(models.Model): class CommunityList(models.Model):
user = ForeignKey(User, blank=True, null=True) person = ForeignKey(Person, blank=True, null=True)
group = ForeignKey(Group, blank=True, null=True) group = ForeignKey(Group, blank=True, null=True)
added_docs = models.ManyToManyField(Document) added_docs = models.ManyToManyField(Document)
def long_name(self): def long_name(self):
if self.user: if self.person:
return 'Personal I-D list of %s' % self.user.username return 'Personal I-D list of %s' % self.person.plain_name()
elif self.group: elif self.group:
return 'I-D list for %s' % self.group.name return 'I-D list for %s' % self.group.name
else: else:
@ -30,8 +29,8 @@ class CommunityList(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
import ietf.community.views import ietf.community.views
if self.user: if self.person:
return urlreverse(ietf.community.views.view_list, kwargs={ 'username': self.user.username }) return urlreverse(ietf.community.views.view_list, kwargs={ 'email_or_name': self.person.email() })
elif self.group: elif self.group:
return urlreverse("ietf.group.views.group_documents", kwargs={ 'acronym': self.group.acronym }) return urlreverse("ietf.group.views.group_documents", kwargs={ 'acronym': self.group.acronym })
return "" return ""

View file

@ -1,13 +1,10 @@
# Copyright The IETF Trust 2016-2020, All Rights Reserved # Copyright The IETF Trust 2016-2023, All Rights Reserved
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from pyquery import PyQuery from pyquery import PyQuery
from django.urls import reverse as urlreverse from django.urls import reverse as urlreverse
from django.contrib.auth.models import User
from django_webtest import WebTest
import debug # pyflakes:ignore import debug # pyflakes:ignore
@ -19,14 +16,14 @@ from ietf.group.models import Group
from ietf.group.utils import setup_default_community_list_for_group from ietf.group.utils import setup_default_community_list_for_group
from ietf.doc.models import State from ietf.doc.models import State
from ietf.doc.utils import add_state_change_event from ietf.doc.utils import add_state_change_event
from ietf.person.models import Person, Email from ietf.person.models import Person, Email, Alias
from ietf.utils.test_utils import login_testing_unauthorized from ietf.utils.test_utils import TestCase, login_testing_unauthorized
from ietf.utils.mail import outbox from ietf.utils.mail import outbox
from ietf.doc.factories import WgDraftFactory from ietf.doc.factories import WgDraftFactory
from ietf.group.factories import GroupFactory, RoleFactory from ietf.group.factories import GroupFactory, RoleFactory
from ietf.person.factories import PersonFactory from ietf.person.factories import PersonFactory, EmailFactory, AliasFactory
class CommunityListTests(WebTest): class CommunityListTests(TestCase):
def test_rule_matching(self): def test_rule_matching(self):
plain = PersonFactory(user__username='plain') plain = PersonFactory(user__username='plain')
ad = Person.objects.get(user__username='ad') ad = Person.objects.get(user__username='ad')
@ -38,7 +35,7 @@ class CommunityListTests(WebTest):
states=[('draft-iesg','lc'),('draft','active')], states=[('draft-iesg','lc'),('draft','active')],
) )
clist = CommunityList.objects.create(user=User.objects.get(username="plain")) clist = CommunityList.objects.create(person=plain)
rule_group = SearchRule.objects.create(rule_type="group", group=draft.group, state=State.objects.get(type="draft", slug="active"), community_list=clist) rule_group = SearchRule.objects.create(rule_type="group", group=draft.group, state=State.objects.get(type="draft", slug="active"), community_list=clist)
rule_group_rfc = SearchRule.objects.create(rule_type="group_rfc", group=draft.group, state=State.objects.get(type="rfc", slug="published"), community_list=clist) rule_group_rfc = SearchRule.objects.create(rule_type="group_rfc", group=draft.group, state=State.objects.get(type="rfc", slug="published"), community_list=clist)
@ -89,18 +86,38 @@ class CommunityListTests(WebTest):
# rule -> docs # rule -> docs
self.assertTrue(draft in list(docs_matching_community_list_rule(rule_group_exp))) self.assertTrue(draft in list(docs_matching_community_list_rule(rule_group_exp)))
def test_view_list_duplicates(self):
person = PersonFactory(name="John Q. Public", user__username="bazquux@example.com")
PersonFactory(name="John Q. Public", user__username="foobar@example.com")
url = urlreverse(ietf.community.views.view_list, kwargs={ "email_or_name": person.plain_name()})
r = self.client.get(url)
self.assertEqual(r.status_code, 300)
self.assertIn("bazquux@example.com", r.content.decode())
self.assertIn("foobar@example.com", r.content.decode())
def complex_person(self, *args, **kwargs):
person = PersonFactory(*args, **kwargs)
EmailFactory(person=person)
AliasFactory(person=person)
return person
def email_or_name_set(self, person):
return [e for e in Email.objects.filter(person=person)] + \
[a for a in Alias.objects.filter(person=person)]
def test_view_list(self): def test_view_list(self):
PersonFactory(user__username='plain') person = self.complex_person(user__username='plain')
draft = WgDraftFactory() draft = WgDraftFactory()
url = urlreverse(ietf.community.views.view_list, kwargs={ "username": "plain" })
# without list # without list
r = self.client.get(url) for id in self.email_or_name_set(person):
self.assertEqual(r.status_code, 200) url = urlreverse(ietf.community.views.view_list, kwargs={ "email_or_name": id })
r = self.client.get(url)
self.assertEqual(r.status_code, 200, msg=f"id='{id}', url='{url}'")
# with list # with list
clist = CommunityList.objects.create(user=User.objects.get(username="plain")) clist = CommunityList.objects.create(person=person)
if not draft in clist.added_docs.all(): if not draft in clist.added_docs.all():
clist.added_docs.add(draft) clist.added_docs.add(draft)
SearchRule.objects.create( SearchRule.objects.create(
@ -109,80 +126,87 @@ class CommunityListTests(WebTest):
state=State.objects.get(type="draft", slug="active"), state=State.objects.get(type="draft", slug="active"),
text="test", text="test",
) )
r = self.client.get(url) for id in self.email_or_name_set(person):
self.assertEqual(r.status_code, 200) url = urlreverse(ietf.community.views.view_list, kwargs={ "email_or_name": id })
self.assertContains(r, draft.name) r = self.client.get(url)
self.assertEqual(r.status_code, 200, msg=f"id='{id}', url='{url}'")
self.assertContains(r, draft.name)
def test_manage_personal_list(self): def test_manage_personal_list(self):
person = self.complex_person(user__username='plain')
PersonFactory(user__username='plain')
ad = Person.objects.get(user__username='ad') ad = Person.objects.get(user__username='ad')
draft = WgDraftFactory(authors=[ad]) draft = WgDraftFactory(authors=[ad])
url = urlreverse(ietf.community.views.manage_list, kwargs={ "username": "plain" }) url = urlreverse(ietf.community.views.manage_list, kwargs={ "email_or_name": person.email() })
login_testing_unauthorized(self, "plain", url) login_testing_unauthorized(self, "plain", url)
page = self.app.get(url, user='plain') for id in self.email_or_name_set(person):
self.assertEqual(page.status_int, 200) url = urlreverse(ietf.community.views.manage_list, kwargs={ "email_or_name": id })
r = self.client.get(url, user='plain')
self.assertEqual(r.status_code, 200, msg=f"id='{id}', url='{url}'")
# add document # We can't call post() with follow=True because that 404's if
self.assertIn('add_document', page.forms) # the url contains unicode, because the django test client
form = page.forms['add_document'] # apparently re-encodes the already-encoded url.
form['documents'].options=[(draft.pk, True, draft.name)] def follow(r):
page = form.submit('action',value='add_documents') redirect_url = r.url or url
self.assertEqual(page.status_int, 302) return self.client.get(redirect_url, user='plain')
clist = CommunityList.objects.get(user__username="plain")
self.assertTrue(clist.added_docs.filter(pk=draft.pk))
page = page.follow()
self.assertContains(page, draft.name) # add document
self.assertContains(r, 'add_document')
r = self.client.post(url, {'action': 'add_documents', 'documents': draft.pk})
self.assertEqual(r.status_code, 302, msg=f"id='{id}', url='{url}'")
clist = CommunityList.objects.get(person__user__username="plain")
self.assertTrue(clist.added_docs.filter(pk=draft.pk))
r = follow(r)
self.assertContains(r, draft.name, status_code=200)
# remove document # remove document
self.assertIn('remove_document_%s' % draft.pk, page.forms) self.assertContains(r, 'remove_document_%s' % draft.pk)
form = page.forms['remove_document_%s' % draft.pk] r = self.client.post(url, {'action': 'remove_document', 'document': draft.pk})
page = form.submit('action',value='remove_document') self.assertEqual(r.status_code, 302, msg=f"id='{id}', url='{url}'")
self.assertEqual(page.status_int, 302) clist = CommunityList.objects.get(person__user__username="plain")
clist = CommunityList.objects.get(user__username="plain") self.assertTrue(not clist.added_docs.filter(pk=draft.pk))
self.assertTrue(not clist.added_docs.filter(pk=draft.pk)) r = follow(r)
page = page.follow() self.assertNotContains(r, draft.name, status_code=200)
# add rule # add rule
r = self.client.post(url, { r = self.client.post(url, {
"action": "add_rule", "action": "add_rule",
"rule_type": "author_rfc", "rule_type": "author_rfc",
"author_rfc-person": Person.objects.filter(documentauthor__document=draft).first().pk, "author_rfc-person": Person.objects.filter(documentauthor__document=draft).first().pk,
"author_rfc-state": State.objects.get(type="rfc", slug="published").pk, "author_rfc-state": State.objects.get(type="rfc", slug="published").pk,
}) })
self.assertEqual(r.status_code, 302) self.assertEqual(r.status_code, 302, msg=f"id='{id}', url='{url}'")
clist = CommunityList.objects.get(user__username="plain") clist = CommunityList.objects.get(person__user__username="plain")
self.assertTrue(clist.searchrule_set.filter(rule_type="author_rfc")) self.assertTrue(clist.searchrule_set.filter(rule_type="author_rfc"))
# add name_contains rule # add name_contains rule
r = self.client.post(url, { r = self.client.post(url, {
"action": "add_rule", "action": "add_rule",
"rule_type": "name_contains", "rule_type": "name_contains",
"name_contains-text": "draft.*mars", "name_contains-text": "draft.*mars",
"name_contains-state": State.objects.get(type="draft", slug="active").pk, "name_contains-state": State.objects.get(type="draft", slug="active").pk,
}) })
self.assertEqual(r.status_code, 302) self.assertEqual(r.status_code, 302, msg=f"id='{id}', url='{url}'")
clist = CommunityList.objects.get(user__username="plain") clist = CommunityList.objects.get(person__user__username="plain")
self.assertTrue(clist.searchrule_set.filter(rule_type="name_contains")) self.assertTrue(clist.searchrule_set.filter(rule_type="name_contains"))
# rule shows up on GET # rule shows up on GET
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200, msg=f"id='{id}', url='{url}'")
rule = clist.searchrule_set.filter(rule_type="author_rfc").first() rule = clist.searchrule_set.filter(rule_type="author_rfc").first()
q = PyQuery(r.content) q = PyQuery(r.content)
self.assertEqual(len(q('#r%s' % rule.pk)), 1) self.assertEqual(len(q('#r%s' % rule.pk)), 1)
# remove rule # remove rule
r = self.client.post(url, { r = self.client.post(url, {
"action": "remove_rule", "action": "remove_rule",
"rule": rule.pk, "rule": rule.pk,
}) })
clist = CommunityList.objects.get(user__username="plain") clist = CommunityList.objects.get(person__user__username="plain")
self.assertTrue(not clist.searchrule_set.filter(rule_type="author_rfc")) self.assertTrue(not clist.searchrule_set.filter(rule_type="author_rfc"))
def test_manage_group_list(self): def test_manage_group_list(self):
draft = WgDraftFactory(group__acronym='mars') draft = WgDraftFactory(group__acronym='mars')
@ -210,77 +234,84 @@ class CommunityListTests(WebTest):
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
def test_track_untrack_document(self): def test_track_untrack_document(self):
PersonFactory(user__username='plain') person = self.complex_person(user__username='plain')
draft = WgDraftFactory() draft = WgDraftFactory()
url = urlreverse(ietf.community.views.track_document, kwargs={ "username": "plain", "name": draft.name }) url = urlreverse(ietf.community.views.track_document, kwargs={ "email_or_name": person.email(), "name": draft.name })
login_testing_unauthorized(self, "plain", url) login_testing_unauthorized(self, "plain", url)
# track for id in self.email_or_name_set(person):
r = self.client.get(url) url = urlreverse(ietf.community.views.track_document, kwargs={ "email_or_name": id, "name": draft.name })
self.assertEqual(r.status_code, 200)
r = self.client.post(url) # track
self.assertEqual(r.status_code, 302) r = self.client.get(url)
clist = CommunityList.objects.get(user__username="plain") self.assertEqual(r.status_code, 200, msg=f"id='{id}', url='{url}'")
self.assertEqual(list(clist.added_docs.all()), [draft])
# untrack r = self.client.post(url)
url = urlreverse(ietf.community.views.untrack_document, kwargs={ "username": "plain", "name": draft.name }) self.assertEqual(r.status_code, 302, msg=f"id='{id}', url='{url}'")
r = self.client.get(url) clist = CommunityList.objects.get(person__user__username="plain")
self.assertEqual(r.status_code, 200) self.assertEqual(list(clist.added_docs.all()), [draft])
r = self.client.post(url) # untrack
self.assertEqual(r.status_code, 302) url = urlreverse(ietf.community.views.untrack_document, kwargs={ "email_or_name": id, "name": draft.name })
clist = CommunityList.objects.get(user__username="plain") r = self.client.get(url)
self.assertEqual(list(clist.added_docs.all()), []) self.assertEqual(r.status_code, 200, msg=f"id='{id}', url='{url}'")
r = self.client.post(url)
self.assertEqual(r.status_code, 302, msg=f"id='{id}', url='{url}'")
clist = CommunityList.objects.get(person__user__username="plain")
self.assertEqual(list(clist.added_docs.all()), [])
def test_track_untrack_document_through_ajax(self): def test_track_untrack_document_through_ajax(self):
PersonFactory(user__username='plain') person = self.complex_person(user__username='plain')
draft = WgDraftFactory() draft = WgDraftFactory()
url = urlreverse(ietf.community.views.track_document, kwargs={ "username": "plain", "name": draft.name }) url = urlreverse(ietf.community.views.track_document, kwargs={ "email_or_name": person.email(), "name": draft.name })
login_testing_unauthorized(self, "plain", url) login_testing_unauthorized(self, "plain", url)
# track for id in self.email_or_name_set(person):
r = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') url = urlreverse(ietf.community.views.track_document, kwargs={ "email_or_name": id, "name": draft.name })
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()["success"], True)
clist = CommunityList.objects.get(user__username="plain")
self.assertEqual(list(clist.added_docs.all()), [draft])
# untrack # track
url = urlreverse(ietf.community.views.untrack_document, kwargs={ "username": "plain", "name": draft.name }) r = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
r = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(r.status_code, 200, msg=f"id='{id}', url='{url}'")
self.assertEqual(r.status_code, 200) self.assertEqual(r.json()["success"], True)
self.assertEqual(r.json()["success"], True) clist = CommunityList.objects.get(person__user__username="plain")
clist = CommunityList.objects.get(user__username="plain") self.assertEqual(list(clist.added_docs.all()), [draft])
self.assertEqual(list(clist.added_docs.all()), [])
# untrack
url = urlreverse(ietf.community.views.untrack_document, kwargs={ "email_or_name": id, "name": draft.name })
r = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(r.status_code, 200, msg=f"id='{id}', url='{url}'")
self.assertEqual(r.json()["success"], True)
clist = CommunityList.objects.get(person__user__username="plain")
self.assertEqual(list(clist.added_docs.all()), [])
def test_csv(self): def test_csv(self):
PersonFactory(user__username='plain') person = self.complex_person(user__username='plain')
draft = WgDraftFactory() draft = WgDraftFactory()
url = urlreverse(ietf.community.views.export_to_csv, kwargs={ "username": "plain" }) for id in self.email_or_name_set(person):
url = urlreverse(ietf.community.views.export_to_csv, kwargs={ "email_or_name": id })
# without list # without list
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200, msg=f"id='{id}', url='{url}'")
# with list # with list
clist = CommunityList.objects.create(user=User.objects.get(username="plain")) clist = CommunityList.objects.create(person=person)
if not draft in clist.added_docs.all(): if not draft in clist.added_docs.all():
clist.added_docs.add(draft) clist.added_docs.add(draft)
SearchRule.objects.create( SearchRule.objects.create(
community_list=clist, community_list=clist,
rule_type="name_contains", rule_type="name_contains",
state=State.objects.get(type="draft", slug="active"), state=State.objects.get(type="draft", slug="active"),
text="test", text="test",
) )
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200, msg=f"id='{id}', url='{url}'")
# this is a simple-minded test, we don't actually check the fields # this is a simple-minded test, we don't actually check the fields
self.assertContains(r, draft.name) self.assertContains(r, draft.name)
def test_csv_for_group(self): def test_csv_for_group(self):
draft = WgDraftFactory() draft = WgDraftFactory()
@ -294,33 +325,34 @@ class CommunityListTests(WebTest):
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
def test_feed(self): def test_feed(self):
PersonFactory(user__username='plain') person = self.complex_person(user__username='plain')
draft = WgDraftFactory() draft = WgDraftFactory()
url = urlreverse(ietf.community.views.feed, kwargs={ "username": "plain" }) for id in self.email_or_name_set(person):
url = urlreverse(ietf.community.views.feed, kwargs={ "email_or_name": id })
# without list # without list
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200, msg=f"id='{id}', url='{url}'")
# with list # with list
clist = CommunityList.objects.create(user=User.objects.get(username="plain")) clist = CommunityList.objects.create(person=person)
if not draft in clist.added_docs.all(): if not draft in clist.added_docs.all():
clist.added_docs.add(draft) clist.added_docs.add(draft)
SearchRule.objects.create( SearchRule.objects.create(
community_list=clist, community_list=clist,
rule_type="name_contains", rule_type="name_contains",
state=State.objects.get(type="draft", slug="active"), state=State.objects.get(type="draft", slug="active"),
text="test", text="test",
) )
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200, msg=f"id='{id}', url='{url}'")
self.assertContains(r, draft.name) self.assertContains(r, draft.name)
# only significant # only significant
r = self.client.get(url + "?significant=1") r = self.client.get(url + "?significant=1")
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200, msg=f"id='{id}', url='{url}'")
self.assertNotContains(r, '<entry>') self.assertNotContains(r, '<entry>')
def test_feed_for_group(self): def test_feed_for_group(self):
draft = WgDraftFactory() draft = WgDraftFactory()
@ -334,19 +366,21 @@ class CommunityListTests(WebTest):
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
def test_subscription(self): def test_subscription(self):
PersonFactory(user__username='plain') person = self.complex_person(user__username='plain')
draft = WgDraftFactory() draft = WgDraftFactory()
url = urlreverse(ietf.community.views.subscription, kwargs={ "username": "plain" }) url = urlreverse(ietf.community.views.subscription, kwargs={ "email_or_name": person.email() })
login_testing_unauthorized(self, "plain", url) login_testing_unauthorized(self, "plain", url)
# subscription without list for id in self.email_or_name_set(person):
r = self.client.get(url) url = urlreverse(ietf.community.views.subscription, kwargs={ "email_or_name": id })
self.assertEqual(r.status_code, 404)
# subscription without list
r = self.client.get(url)
self.assertEqual(r.status_code, 404, msg=f"id='{id}', url='{url}'")
# subscription with list # subscription with list
clist = CommunityList.objects.create(user=User.objects.get(username="plain")) clist = CommunityList.objects.create(person=person)
if not draft in clist.added_docs.all(): if not draft in clist.added_docs.all():
clist.added_docs.add(draft) clist.added_docs.add(draft)
SearchRule.objects.create( SearchRule.objects.create(
@ -355,22 +389,25 @@ class CommunityListTests(WebTest):
state=State.objects.get(type="draft", slug="active"), state=State.objects.get(type="draft", slug="active"),
text="test", text="test",
) )
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# subscribe for email in Email.objects.filter(person=person):
email = Email.objects.filter(person__user__username="plain").first() url = urlreverse(ietf.community.views.subscription, kwargs={ "email_or_name": email })
r = self.client.post(url, { "email": email.pk, "notify_on": "significant", "action": "subscribe" })
self.assertEqual(r.status_code, 302)
subscription = EmailSubscription.objects.filter(community_list=clist, email=email, notify_on="significant").first() r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue(subscription) # subscribe
r = self.client.post(url, { "email": email.pk, "notify_on": "significant", "action": "subscribe" })
self.assertEqual(r.status_code, 302)
# delete subscription subscription = EmailSubscription.objects.filter(community_list=clist, email=email, notify_on="significant").first()
r = self.client.post(url, { "subscription_id": subscription.pk, "action": "unsubscribe" })
self.assertEqual(r.status_code, 302) self.assertTrue(subscription)
self.assertEqual(EmailSubscription.objects.filter(community_list=clist, email=email, notify_on="significant").count(), 0)
# delete subscription
r = self.client.post(url, { "subscription_id": subscription.pk, "action": "unsubscribe" })
self.assertEqual(r.status_code, 302)
self.assertEqual(EmailSubscription.objects.filter(community_list=clist, email=email, notify_on="significant").count(), 0)
def test_subscription_for_group(self): def test_subscription_for_group(self):
draft = WgDraftFactory(group__acronym='mars') draft = WgDraftFactory(group__acronym='mars')
@ -385,12 +422,12 @@ class CommunityListTests(WebTest):
# test GET, rest is tested with personal list # test GET, rest is tested with personal list
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
def test_notification(self): def test_notification(self):
PersonFactory(user__username='plain') person = PersonFactory(user__username='plain')
draft = WgDraftFactory() draft = WgDraftFactory()
clist = CommunityList.objects.create(user=User.objects.get(username="plain")) clist = CommunityList.objects.create(person=person)
if not draft in clist.added_docs.all(): if not draft in clist.added_docs.all():
clist.added_docs.add(draft) clist.added_docs.add(draft)

View file

@ -4,11 +4,11 @@ from ietf.community import views
from ietf.utils.urls import url from ietf.utils.urls import url
urlpatterns = [ urlpatterns = [
url(r'^personal/(?P<username>[^/]+)/$', views.view_list), url(r'^personal/(?P<email_or_name>[^/]+)/$', views.view_list),
url(r'^personal/(?P<username>[^/]+)/manage/$', views.manage_list), url(r'^personal/(?P<email_or_name>[^/]+)/manage/$', views.manage_list),
url(r'^personal/(?P<username>[^/]+)/trackdocument/(?P<name>[^/]+)/$', views.track_document), url(r'^personal/(?P<email_or_name>[^/]+)/trackdocument/(?P<name>[^/]+)/$', views.track_document),
url(r'^personal/(?P<username>[^/]+)/untrackdocument/(?P<name>[^/]+)/$', views.untrack_document), url(r'^personal/(?P<email_or_name>[^/]+)/untrackdocument/(?P<name>[^/]+)/$', views.untrack_document),
url(r'^personal/(?P<username>[^/]+)/csv/$', views.export_to_csv), url(r'^personal/(?P<email_or_name>[^/]+)/csv/$', views.export_to_csv),
url(r'^personal/(?P<username>[^/]+)/feed/$', views.feed), url(r'^personal/(?P<email_or_name>[^/]+)/feed/$', views.feed),
url(r'^personal/(?P<username>[^/]+)/subscription/$', views.subscription), url(r'^personal/(?P<email_or_name>[^/]+)/subscription/$', views.subscription),
] ]

View file

@ -1,4 +1,4 @@
# Copyright The IETF Trust 2016-2020, All Rights Reserved # Copyright The IETF Trust 2016-2023, All Rights Reserved
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
@ -11,11 +11,9 @@ import debug # pyflakes:ignore
from ietf.community.models import CommunityList, EmailSubscription, SearchRule from ietf.community.models import CommunityList, EmailSubscription, SearchRule
from ietf.doc.models import Document, State from ietf.doc.models import Document, State
from ietf.group.models import Role, Group from ietf.group.models import Role
from ietf.person.models import Person from ietf.person.models import Person
from ietf.ietfauth.utils import has_role from ietf.ietfauth.utils import has_role
from django.contrib.auth.models import User
from django.shortcuts import get_object_or_404
from ietf.utils.mail import send_mail from ietf.utils.mail import send_mail
@ -29,24 +27,12 @@ def states_of_significant_change():
Q(type="draft", slug__in=['rfc', 'dead']) Q(type="draft", slug__in=['rfc', 'dead'])
) )
def lookup_community_list(username=None, acronym=None):
assert username or acronym
if acronym:
group = get_object_or_404(Group, acronym=acronym)
clist = CommunityList.objects.filter(group=group).first() or CommunityList(group=group)
else:
user = get_object_or_404(User, username__iexact=username)
clist = CommunityList.objects.filter(user=user).first() or CommunityList(user=user)
return clist
def can_manage_community_list(user, clist): def can_manage_community_list(user, clist):
if not user or not user.is_authenticated: if not user or not user.is_authenticated:
return False return False
if clist.user: if clist.person:
return user == clist.user return user == clist.person.user
elif clist.group: elif clist.group:
if has_role(user, 'Secretariat'): if has_role(user, 'Secretariat'):
return True return True

View file

@ -1,4 +1,4 @@
# Copyright The IETF Trust 2012-2020, All Rights Reserved # Copyright The IETF Trust 2012-2023, All Rights Reserved
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
@ -15,18 +15,46 @@ from django.utils.html import strip_tags
import debug # pyflakes:ignore import debug # pyflakes:ignore
from ietf.community.models import SearchRule, EmailSubscription from ietf.community.models import CommunityList, EmailSubscription, SearchRule
from ietf.community.forms import SearchRuleTypeForm, SearchRuleForm, AddDocumentsForm, SubscriptionForm from ietf.community.forms import SearchRuleTypeForm, SearchRuleForm, AddDocumentsForm, SubscriptionForm
from ietf.community.utils import lookup_community_list, can_manage_community_list from ietf.community.utils import can_manage_community_list
from ietf.community.utils import docs_tracked_by_community_list, docs_matching_community_list_rule from ietf.community.utils import docs_tracked_by_community_list, docs_matching_community_list_rule
from ietf.community.utils import states_of_significant_change, reset_name_contains_index_for_rule from ietf.community.utils import states_of_significant_change, reset_name_contains_index_for_rule
from ietf.group.models import Group
from ietf.doc.models import DocEvent, Document from ietf.doc.models import DocEvent, Document
from ietf.doc.utils_search import prepare_document_table from ietf.doc.utils_search import prepare_document_table
from ietf.person.utils import lookup_persons
from ietf.utils.http import is_ajax from ietf.utils.http import is_ajax
from ietf.utils.response import permission_denied from ietf.utils.response import permission_denied
def view_list(request, username=None): class MultiplePersonError(Exception):
clist = lookup_community_list(username) """More than one Person record matches the given email or name"""
pass
def lookup_community_list(request, email_or_name=None, acronym=None):
assert email_or_name or acronym
if acronym:
group = get_object_or_404(Group, acronym=acronym)
clist = CommunityList.objects.filter(group=group).first() or CommunityList(group=group)
else:
persons = lookup_persons(email_or_name)
if len(persons) > 1:
if hasattr(request.user, 'person') and request.user.person in persons:
person = request.user.person
else:
raise MultiplePersonError("\r\n".join([p.user.username for p in persons]))
else:
person = persons[0]
clist = CommunityList.objects.filter(person=person).first() or CommunityList(person=person)
return clist
def view_list(request, email_or_name=None):
try:
clist = lookup_community_list(request, email_or_name)
except MultiplePersonError as err:
return HttpResponse(str(err), status=300)
docs = docs_tracked_by_community_list(clist) docs = docs_tracked_by_community_list(clist)
docs, meta = prepare_document_table(request, docs, request.GET) docs, meta = prepare_document_table(request, docs, request.GET)
@ -42,10 +70,13 @@ def view_list(request, username=None):
}) })
@login_required @login_required
def manage_list(request, username=None, acronym=None, group_type=None): def manage_list(request, email_or_name=None, acronym=None):
# we need to be a bit careful because clist may not exist in the # we need to be a bit careful because clist may not exist in the
# database so we can't call related stuff on it yet # database so we can't call related stuff on it yet
clist = lookup_community_list(username, acronym) try:
clist = lookup_community_list(request, email_or_name, acronym)
except MultiplePersonError as err:
return HttpResponse(str(err), status=300)
if not can_manage_community_list(request.user, clist): if not can_manage_community_list(request.user, clist):
permission_denied(request, "You do not have permission to access this view") permission_denied(request, "You do not have permission to access this view")
@ -128,11 +159,14 @@ def manage_list(request, username=None, acronym=None, group_type=None):
@login_required @login_required
def track_document(request, name, username=None, acronym=None): def track_document(request, name, email_or_name=None, acronym=None):
doc = get_object_or_404(Document, name=name) doc = get_object_or_404(Document, name=name)
if request.method == "POST": if request.method == "POST":
clist = lookup_community_list(username, acronym) try:
clist = lookup_community_list(request, email_or_name, acronym)
except MultiplePersonError as err:
return HttpResponse(str(err), status=300)
if not can_manage_community_list(request.user, clist): if not can_manage_community_list(request.user, clist):
permission_denied(request, "You do not have permission to access this view") permission_denied(request, "You do not have permission to access this view")
@ -152,9 +186,12 @@ def track_document(request, name, username=None, acronym=None):
}) })
@login_required @login_required
def untrack_document(request, name, username=None, acronym=None): def untrack_document(request, name, email_or_name=None, acronym=None):
doc = get_object_or_404(Document, name=name) doc = get_object_or_404(Document, name=name)
clist = lookup_community_list(username, acronym) try:
clist = lookup_community_list(request, email_or_name, acronym)
except MultiplePersonError as err:
return HttpResponse(str(err), status=300)
if not can_manage_community_list(request.user, clist): if not can_manage_community_list(request.user, clist):
permission_denied(request, "You do not have permission to access this view") permission_denied(request, "You do not have permission to access this view")
@ -172,8 +209,11 @@ def untrack_document(request, name, username=None, acronym=None):
}) })
def export_to_csv(request, username=None, acronym=None, group_type=None): def export_to_csv(request, email_or_name=None, acronym=None):
clist = lookup_community_list(username, acronym) try:
clist = lookup_community_list(request, email_or_name, acronym)
except MultiplePersonError as err:
return HttpResponse(str(err), status=300)
response = HttpResponse(content_type='text/csv') response = HttpResponse(content_type='text/csv')
@ -213,8 +253,11 @@ def export_to_csv(request, username=None, acronym=None, group_type=None):
return response return response
def feed(request, username=None, acronym=None, group_type=None): def feed(request, email_or_name=None, acronym=None):
clist = lookup_community_list(username, acronym) try:
clist = lookup_community_list(request, email_or_name, acronym)
except MultiplePersonError as err:
return HttpResponse(str(err), status=300)
significant = request.GET.get('significant', '') == '1' significant = request.GET.get('significant', '') == '1'
@ -249,17 +292,22 @@ def feed(request, username=None, acronym=None, group_type=None):
@login_required @login_required
def subscription(request, username=None, acronym=None, group_type=None): def subscription(request, email_or_name=None, acronym=None):
clist = lookup_community_list(username, acronym) try:
if clist.pk is None: clist = lookup_community_list(request, email_or_name, acronym)
raise Http404 if clist.pk is None:
raise Http404
except MultiplePersonError as err:
return HttpResponse(str(err), status=300)
existing_subscriptions = EmailSubscription.objects.filter(community_list=clist, email__person__user=request.user) person = request.user.person
existing_subscriptions = EmailSubscription.objects.filter(community_list=clist, email__person=person)
if request.method == 'POST': if request.method == 'POST':
action = request.POST.get("action") action = request.POST.get("action")
if action == "subscribe": if action == "subscribe":
form = SubscriptionForm(request.user, clist, request.POST) form = SubscriptionForm(person, clist, request.POST)
if form.is_valid(): if form.is_valid():
subscription = form.save(commit=False) subscription = form.save(commit=False)
subscription.community_list = clist subscription.community_list = clist
@ -272,7 +320,7 @@ def subscription(request, username=None, acronym=None, group_type=None):
return HttpResponseRedirect("") return HttpResponseRedirect("")
else: else:
form = SubscriptionForm(request.user, clist) form = SubscriptionForm(person, clist)
return render(request, 'community/subscription.html', { return render(request, 'community/subscription.html', {
'clist': clist, 'clist': clist,

View file

@ -1081,29 +1081,26 @@ def build_file_urls(doc: Union[Document, DocHistory]):
return file_urls, found_types return file_urls, found_types
def augment_docs_and_user_with_user_info(docs, user): def augment_docs_and_person_with_person_info(docs, person):
"""Add attribute to each document with whether the document is tracked """Add attribute to each document with whether the document is tracked
or has a review wish by the user or not, and the review teams the user is on.""" or has a review wish by the person or not, and the review teams the person is on."""
tracked = set() tracked = set()
review_wished = set() review_wished = set()
if user and user.is_authenticated:
user.review_teams = Group.objects.filter(
reviewteamsettings__isnull=False, role__person__user=user, role__name='reviewer')
doc_pks = [d.pk for d in docs] # used in templates
clist = CommunityList.objects.filter(user=user).first() person.review_teams = Group.objects.filter(
if clist: reviewteamsettings__isnull=False, role__person=person, role__name='reviewer')
tracked.update(
docs_tracked_by_community_list(clist).filter(pk__in=doc_pks).values_list("pk", flat=True))
try: doc_pks = [d.pk for d in docs]
wishes = ReviewWish.objects.filter(person=Person.objects.get(user=user)) clist = CommunityList.objects.filter(person=person).first()
wishes = wishes.filter(doc__pk__in=doc_pks).values_list("doc__pk", flat=True) if clist:
review_wished.update(wishes) tracked.update(
except Person.DoesNotExist: docs_tracked_by_community_list(clist).filter(pk__in=doc_pks).values_list("pk", flat=True))
pass
wishes = ReviewWish.objects.filter(person=person)
wishes = wishes.filter(doc__pk__in=doc_pks).values_list("doc__pk", flat=True)
review_wished.update(wishes)
for d in docs: for d in docs:
d.tracked_in_personal_community_list = d.pk in tracked d.tracked_in_personal_community_list = d.pk in tracked

View file

@ -11,7 +11,7 @@ from django.conf import settings
from ietf.doc.models import Document, RelatedDocument, DocEvent, TelechatDocEvent, BallotDocEvent, DocTypeName from ietf.doc.models import Document, RelatedDocument, DocEvent, TelechatDocEvent, BallotDocEvent, DocTypeName
from ietf.doc.expire import expirable_drafts from ietf.doc.expire import expirable_drafts
from ietf.doc.utils import augment_docs_and_user_with_user_info from ietf.doc.utils import augment_docs_and_person_with_person_info
from ietf.meeting.models import SessionPresentation, Meeting, Session from ietf.meeting.models import SessionPresentation, Meeting, Session
from ietf.review.utils import review_assignments_to_list_for_docs from ietf.review.utils import review_assignments_to_list_for_docs
from ietf.utils.timezone import date_today from ietf.utils.timezone import date_today
@ -199,7 +199,8 @@ def prepare_document_table(request, docs, query=None, max_results=200, show_ad_a
docs = docs[:max_results] docs = docs[:max_results]
fill_in_document_table_attributes(docs) fill_in_document_table_attributes(docs)
augment_docs_and_user_with_user_info(docs, request.user) if request.user.is_authenticated and hasattr(request.user, "person"):
augment_docs_and_person_with_person_info(docs, request.user.person)
augment_docs_with_related_docs_info(docs) augment_docs_with_related_docs_info(docs)
meta = {} meta = {}

View file

@ -62,7 +62,7 @@ from ietf.doc.utils import (augment_events_with_revision,
needed_ballot_positions, nice_consensus, update_telechat, has_same_ballot, needed_ballot_positions, nice_consensus, update_telechat, has_same_ballot,
get_initial_notify, make_notify_changed_event, make_rev_history, default_consensus, get_initial_notify, make_notify_changed_event, make_rev_history, default_consensus,
add_events_message_info, get_unicode_document_content, add_events_message_info, get_unicode_document_content,
augment_docs_and_user_with_user_info, irsg_needed_ballot_positions, add_action_holder_change_event, augment_docs_and_person_with_person_info, irsg_needed_ballot_positions, add_action_holder_change_event,
build_file_urls, update_documentauthors, fuzzy_find_documents, build_file_urls, update_documentauthors, fuzzy_find_documents,
bibxml_for_draft) bibxml_for_draft)
from ietf.doc.utils_bofreq import bofreq_editors, bofreq_responsible from ietf.doc.utils_bofreq import bofreq_editors, bofreq_responsible
@ -287,7 +287,8 @@ def document_main(request, name, rev=None, document_html=False):
presentations = doc.future_presentations() presentations = doc.future_presentations()
augment_docs_and_user_with_user_info([doc], request.user) if request.user.is_authenticated and hasattr(request.user, "person"):
augment_docs_and_person_with_person_info([doc], request.user.person)
exp_comment = doc.latest_event(IanaExpertDocEvent,type="comment") exp_comment = doc.latest_event(IanaExpertDocEvent,type="comment")
iana_experts_comment = exp_comment and exp_comment.desc iana_experts_comment = exp_comment and exp_comment.desc
@ -580,7 +581,8 @@ def document_main(request, name, rev=None, document_html=False):
elif can_edit_stream_info and (iesg_state_slug in ('idexists','watching')): elif can_edit_stream_info and (iesg_state_slug in ('idexists','watching')):
actions.append(("Submit to IESG for Publication", urlreverse('ietf.doc.views_draft.to_iesg', kwargs=dict(name=doc.name)))) actions.append(("Submit to IESG for Publication", urlreverse('ietf.doc.views_draft.to_iesg', kwargs=dict(name=doc.name))))
augment_docs_and_user_with_user_info([doc], request.user) if request.user.is_authenticated and hasattr(request.user, "person"):
augment_docs_and_person_with_person_info([doc], request.user.person)
published = doc.latest_event(type="published_rfc") # todo rethink this now that published_rfc is on rfc published = doc.latest_event(type="published_rfc") # todo rethink this now that published_rfc is on rfc
started_iesg_process = doc.latest_event(type="started_iesg_process") started_iesg_process = doc.latest_event(type="started_iesg_process")

View file

@ -21,9 +21,9 @@ class NomComAdmin(admin.ModelAdmin):
admin.site.register(NomCom, NomComAdmin) admin.site.register(NomCom, NomComAdmin)
class NominationAdmin(admin.ModelAdmin): class NominationAdmin(admin.ModelAdmin):
list_display = ['id', 'position', 'candidate_name', 'candidate_email', 'candidate_phone', 'nominee', 'comments', 'nominator_email', 'user', 'time', 'share_nominator'] list_display = ['id', 'position', 'candidate_name', 'candidate_email', 'candidate_phone', 'nominee', 'comments', 'nominator_email', 'person', 'time', 'share_nominator']
list_filter = ['time', 'share_nominator'] list_filter = ['time', 'share_nominator']
raw_id_fields = ['nominee', 'comments', 'user'] raw_id_fields = ['nominee', 'comments', 'person']
admin.site.register(Nomination, NominationAdmin) admin.site.register(Nomination, NominationAdmin)
class NomineeAdmin(admin.ModelAdmin): class NomineeAdmin(admin.ModelAdmin):
@ -51,9 +51,9 @@ class FeedbackAdmin(admin.ModelAdmin):
return ", ".join(n.person.ascii for n in obj.nominees.all()) return ", ".join(n.person.ascii for n in obj.nominees.all())
nominee.admin_order_field = 'nominees__person__ascii' # type: ignore # https://github.com/python/mypy/issues/2087 nominee.admin_order_field = 'nominees__person__ascii' # type: ignore # https://github.com/python/mypy/issues/2087
list_display = ['id', 'nomcom', 'author', 'nominee', 'subject', 'type', 'user', 'time'] list_display = ['id', 'nomcom', 'author', 'nominee', 'subject', 'type', 'person', 'time']
list_filter = ['nomcom', 'type', 'time', ] list_filter = ['nomcom', 'type', 'time', ]
raw_id_fields = ['positions', 'topics', 'user'] raw_id_fields = ['positions', 'topics', 'person']
admin.site.register(Feedback, FeedbackAdmin) admin.site.register(Feedback, FeedbackAdmin)

View file

@ -9,7 +9,7 @@ from faker import Faker
from ietf.nomcom.models import NomCom, Position, Feedback, Nominee, NomineePosition, Nomination, Topic from ietf.nomcom.models import NomCom, Position, Feedback, Nominee, NomineePosition, Nomination, Topic
from ietf.group.factories import GroupFactory from ietf.group.factories import GroupFactory
from ietf.person.factories import PersonFactory, UserFactory from ietf.person.factories import PersonFactory
import debug # pyflakes:ignore import debug # pyflakes:ignore
@ -199,7 +199,7 @@ class NominationFactory(factory.django.DjangoModelFactory):
candidate_email = factory.LazyAttribute(lambda obj: obj.nominee.person.email()) candidate_email = factory.LazyAttribute(lambda obj: obj.nominee.person.email())
candidate_phone = factory.Faker('phone_number') candidate_phone = factory.Faker('phone_number')
comments = factory.SubFactory(FeedbackFactory) comments = factory.SubFactory(FeedbackFactory)
nominator_email = factory.LazyAttribute(lambda obj: obj.user.email) nominator_email = factory.LazyAttribute(lambda obj: obj.person.user.email)
user = factory.SubFactory(UserFactory) person = factory.SubFactory(PersonFactory)
share_nominator = False share_nominator = False

View file

@ -15,7 +15,7 @@ from ietf.name.models import FeedbackTypeName, NomineePositionStateName
from ietf.nomcom.models import ( NomCom, Nomination, Nominee, NomineePosition, from ietf.nomcom.models import ( NomCom, Nomination, Nominee, NomineePosition,
Position, Feedback, ReminderDates, Topic, Volunteer ) Position, Feedback, ReminderDates, Topic, Volunteer )
from ietf.nomcom.utils import (NOMINATION_RECEIPT_TEMPLATE, FEEDBACK_RECEIPT_TEMPLATE, from ietf.nomcom.utils import (NOMINATION_RECEIPT_TEMPLATE, FEEDBACK_RECEIPT_TEMPLATE,
get_user_email, validate_private_key, validate_public_key, get_person_email, validate_private_key, validate_public_key,
make_nomineeposition, make_nomineeposition_for_newperson, make_nomineeposition, make_nomineeposition_for_newperson,
create_feedback_email) create_feedback_email)
from ietf.person.models import Email from ietf.person.models import Email
@ -256,7 +256,7 @@ class NominateForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.nomcom = kwargs.pop('nomcom', None) self.nomcom = kwargs.pop('nomcom', None)
self.user = kwargs.pop('user', None) self.person = kwargs.pop('person', None)
self.public = kwargs.pop('public', None) self.public = kwargs.pop('public', None)
super(NominateForm, self).__init__(*args, **kwargs) super(NominateForm, self).__init__(*args, **kwargs)
@ -273,7 +273,7 @@ class NominateForm(forms.ModelForm):
if not self.public: if not self.public:
self.fields.pop('confirmation') self.fields.pop('confirmation')
author = get_user_email(self.user) author = get_person_email(self.person)
if author: if author:
self.fields['nominator_email'].initial = author.address 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 help_text = """(Nomcom Chair/Member: please fill this in. Use your own email address if the person making the
@ -303,7 +303,7 @@ class NominateForm(forms.ModelForm):
author = None author = None
if self.public: if self.public:
author = get_user_email(self.user) author = get_person_email(self.person)
else: else:
if nominator_email: if nominator_email:
emails = Email.objects.filter(address=nominator_email) emails = Email.objects.filter(address=nominator_email)
@ -314,7 +314,7 @@ class NominateForm(forms.ModelForm):
feedback = Feedback.objects.create(nomcom=self.nomcom, feedback = Feedback.objects.create(nomcom=self.nomcom,
comments=self.nomcom.encrypt(qualifications), comments=self.nomcom.encrypt(qualifications),
type=FeedbackTypeName.objects.get(slug='nomina'), type=FeedbackTypeName.objects.get(slug='nomina'),
user=self.user) person=self.person)
feedback.positions.add(position) feedback.positions.add(position)
feedback.nominees.add(nominee) feedback.nominees.add(nominee)
@ -326,7 +326,7 @@ class NominateForm(forms.ModelForm):
nomination.nominee = nominee nomination.nominee = nominee
nomination.comments = feedback nomination.comments = feedback
nomination.share_nominator = share_nominator nomination.share_nominator = share_nominator
nomination.user = self.user nomination.person = self.person
if commit: if commit:
nomination.save() nomination.save()
@ -361,7 +361,7 @@ class NominateNewPersonForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.nomcom = kwargs.pop('nomcom', None) self.nomcom = kwargs.pop('nomcom', None)
self.user = kwargs.pop('user', None) self.person = kwargs.pop('person', None)
self.public = kwargs.pop('public', None) self.public = kwargs.pop('public', None)
super(NominateNewPersonForm, self).__init__(*args, **kwargs) super(NominateNewPersonForm, self).__init__(*args, **kwargs)
@ -375,7 +375,7 @@ class NominateNewPersonForm(forms.ModelForm):
if not self.public: if not self.public:
self.fields.pop('confirmation') self.fields.pop('confirmation')
author = get_user_email(self.user) author = get_person_email(self.person)
if author: if author:
self.fields['nominator_email'].initial = author.address 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 help_text = """(Nomcom Chair/Member: please fill this in. Use your own email address if the person making the
@ -416,7 +416,7 @@ class NominateNewPersonForm(forms.ModelForm):
author = None author = None
if self.public: if self.public:
author = get_user_email(self.user) author = get_person_email(self.person)
else: else:
if nominator_email: if nominator_email:
emails = Email.objects.filter(address=nominator_email) emails = Email.objects.filter(address=nominator_email)
@ -429,7 +429,7 @@ class NominateNewPersonForm(forms.ModelForm):
feedback = Feedback.objects.create(nomcom=self.nomcom, feedback = Feedback.objects.create(nomcom=self.nomcom,
comments=self.nomcom.encrypt(qualifications), comments=self.nomcom.encrypt(qualifications),
type=FeedbackTypeName.objects.get(slug='nomina'), type=FeedbackTypeName.objects.get(slug='nomina'),
user=self.user) person=self.person)
feedback.positions.add(position) feedback.positions.add(position)
feedback.nominees.add(nominee) feedback.nominees.add(nominee)
@ -441,7 +441,7 @@ class NominateNewPersonForm(forms.ModelForm):
nomination.nominee = nominee nomination.nominee = nominee
nomination.comments = feedback nomination.comments = feedback
nomination.share_nominator = share_nominator nomination.share_nominator = share_nominator
nomination.user = self.user nomination.person = self.person
if commit: if commit:
nomination.save() nomination.save()
@ -476,7 +476,7 @@ class FeedbackForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.nomcom = kwargs.pop('nomcom', None) self.nomcom = kwargs.pop('nomcom', None)
self.user = kwargs.pop('user', None) self.person = kwargs.pop('person', None)
self.public = kwargs.pop('public', None) self.public = kwargs.pop('public', None)
self.position = kwargs.pop('position', None) self.position = kwargs.pop('position', None)
self.nominee = kwargs.pop('nominee', None) self.nominee = kwargs.pop('nominee', None)
@ -484,7 +484,7 @@ class FeedbackForm(forms.ModelForm):
super(FeedbackForm, self).__init__(*args, **kwargs) super(FeedbackForm, self).__init__(*args, **kwargs)
author = get_user_email(self.user) author = get_person_email(self.person)
if self.public: if self.public:
self.fields.pop('nominator_email') self.fields.pop('nominator_email')
@ -514,7 +514,7 @@ class FeedbackForm(forms.ModelForm):
author = None author = None
if self.public: if self.public:
author = get_user_email(self.user) author = get_person_email(self.person)
else: else:
nominator_email = self.cleaned_data['nominator_email'] nominator_email = self.cleaned_data['nominator_email']
if nominator_email: if nominator_email:
@ -525,7 +525,7 @@ class FeedbackForm(forms.ModelForm):
feedback.author = author.address feedback.author = author.address
feedback.nomcom = self.nomcom feedback.nomcom = self.nomcom
feedback.user = self.user feedback.person = self.person
feedback.type = FeedbackTypeName.objects.get(slug='comment') feedback.type = FeedbackTypeName.objects.get(slug='comment')
feedback.comments = self.nomcom.encrypt(comment_text) feedback.comments = self.nomcom.encrypt(comment_text)
feedback.save() feedback.save()
@ -578,7 +578,7 @@ class QuestionnaireForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.nomcom = kwargs.pop('nomcom', None) self.nomcom = kwargs.pop('nomcom', None)
self.user = kwargs.pop('user', None) self.person = kwargs.pop('person', None)
super(QuestionnaireForm, self).__init__(*args, **kwargs) super(QuestionnaireForm, self).__init__(*args, **kwargs)
self.fields['nominee'] = PositionNomineeField(nomcom=self.nomcom, required=True) self.fields['nominee'] = PositionNomineeField(nomcom=self.nomcom, required=True)
@ -588,13 +588,13 @@ class QuestionnaireForm(forms.ModelForm):
comment_text = self.cleaned_data['comment_text'] comment_text = self.cleaned_data['comment_text']
(position, nominee) = self.cleaned_data['nominee'] (position, nominee) = self.cleaned_data['nominee']
author = get_user_email(self.user) author = get_person_email(self.person)
if author: if author:
feedback.author = author feedback.author = author
feedback.nomcom = self.nomcom feedback.nomcom = self.nomcom
feedback.user = self.user feedback.person = self.person
feedback.type = FeedbackTypeName.objects.get(slug='questio') feedback.type = FeedbackTypeName.objects.get(slug='questio')
feedback.comments = self.nomcom.encrypt(comment_text) feedback.comments = self.nomcom.encrypt(comment_text)
feedback.save() feedback.save()
@ -659,9 +659,9 @@ class PendingFeedbackForm(forms.ModelForm):
model = Feedback model = Feedback
fields = ('type', ) fields = ('type', )
def set_nomcom(self, nomcom, user): def set_nomcom(self, nomcom, person):
self.nomcom = nomcom self.nomcom = nomcom
self.user = user self.person = person
#self.fields['nominee'] = MultiplePositionNomineeField(nomcom=self.nomcom, #self.fields['nominee'] = MultiplePositionNomineeField(nomcom=self.nomcom,
#required=True, #required=True,
#widget=forms.SelectMultiple, #widget=forms.SelectMultiple,
@ -670,7 +670,7 @@ class PendingFeedbackForm(forms.ModelForm):
def save(self, commit=True): def save(self, commit=True):
feedback = super(PendingFeedbackForm, self).save(commit=False) feedback = super(PendingFeedbackForm, self).save(commit=False)
feedback.nomcom = self.nomcom feedback.nomcom = self.nomcom
feedback.user = self.user feedback.person = self.person
feedback.save() feedback.save()
return feedback return feedback
@ -700,9 +700,9 @@ class MutableFeedbackForm(forms.ModelForm):
model = Feedback model = Feedback
fields = ('type', ) fields = ('type', )
def set_nomcom(self, nomcom, user, instances=None): def set_nomcom(self, nomcom, person, instances=None):
self.nomcom = nomcom self.nomcom = nomcom
self.user = user self.person = person
instances = instances or [] instances = instances or []
self.feedback_type = None self.feedback_type = None
for i in instances: for i in instances:
@ -782,7 +782,7 @@ class MutableFeedbackForm(forms.ModelForm):
nominee=nominee, nominee=nominee,
comments=feedback, comments=feedback,
nominator_email=nominator_email, nominator_email=nominator_email,
user=self.user) person=self.person)
return feedback return feedback
else: else:
feedback.save() feedback.save()

View file

@ -0,0 +1,100 @@
# Generated by Django 4.2.2 on 2023-06-14 19:47
from django.db import migrations
from django.db.models import OuterRef, Subquery
import django.db.models.deletion
import ietf.utils.models
def forward(apps, schema_editor):
Nomination = apps.get_model('nomcom', 'Nomination')
Person = apps.get_model("person", "Person")
Nomination.objects.exclude(
user__isnull=True
).update(
person=Subquery(
Person.objects.filter(user_id=OuterRef("user_id")).values("pk")[:1]
)
)
Feedback = apps.get_model('nomcom', 'Feedback')
Feedback.objects.exclude(
user__isnull=True
).update(
person=Subquery(
Person.objects.filter(user_id=OuterRef("user_id")).values("pk")[:1]
)
)
def reverse(apps, schema_editor):
Nomination = apps.get_model('nomcom', 'Nomination')
Person = apps.get_model("person", "Person")
Nomination.objects.exclude(
person__isnull=True
).update(
user_id=Subquery(
Person.objects.filter(pk=OuterRef("person_id")).values("user_id")[:1]
)
)
Feedback = apps.get_model('nomcom', 'Feedback')
Feedback.objects.exclude(
person__isnull=True
).update(
user_id=Subquery(
Person.objects.filter(pk=OuterRef("person_id")).values("user_id")[:1]
)
)
class Migration(migrations.Migration):
dependencies = [
("person", "0001_initial"),
("nomcom", "0004_volunteer_origin_volunteer_time_volunteer_withdrawn"),
]
operations = [
migrations.AddField(
model_name="feedback",
name="person",
field=ietf.utils.models.ForeignKey(
blank=True,
editable=False,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="person.person",
),
),
migrations.AddField(
model_name="nomination",
name="person",
field=ietf.utils.models.ForeignKey(
editable=False,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="person.person",
),
),
migrations.RunPython(forward, reverse),
migrations.RemoveField(
model_name="feedback",
name="user",
field=ietf.utils.models.ForeignKey(
blank=True,
editable=False,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="user.user",
),
),
migrations.RemoveField(
model_name="nomination",
name="user",
field=ietf.utils.models.ForeignKey(
blank=True,
editable=False,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="user.user",
),
),
]

View file

@ -7,7 +7,6 @@ import os
from django.db import models from django.db import models
from django.db.models.signals import post_delete from django.db.models.signals import post_delete
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.template.defaultfilters import linebreaks # type: ignore from django.template.defaultfilters import linebreaks # type: ignore
@ -128,7 +127,7 @@ class Nomination(models.Model):
nominee = ForeignKey('Nominee') nominee = ForeignKey('Nominee')
comments = ForeignKey('Feedback') comments = ForeignKey('Feedback')
nominator_email = models.EmailField(verbose_name='Nominator Email', blank=True) nominator_email = models.EmailField(verbose_name='Nominator Email', blank=True)
user = ForeignKey(User, editable=False, null=True, on_delete=models.SET_NULL) person = ForeignKey(Person, editable=False, null=True, on_delete=models.SET_NULL)
time = models.DateTimeField(auto_now_add=True) time = models.DateTimeField(auto_now_add=True)
share_nominator = models.BooleanField(verbose_name='OK to share nominator\'s name with candidate', default=False, share_nominator = models.BooleanField(verbose_name='OK to share nominator\'s name with candidate', default=False,
help_text='Check this box to allow the NomCom to let the ' help_text='Check this box to allow the NomCom to let the '
@ -299,7 +298,7 @@ class Feedback(models.Model):
subject = models.TextField(verbose_name='Subject', blank=True) subject = models.TextField(verbose_name='Subject', blank=True)
comments = models.BinaryField(verbose_name='Comments') comments = models.BinaryField(verbose_name='Comments')
type = ForeignKey(FeedbackTypeName, blank=True, null=True) type = ForeignKey(FeedbackTypeName, blank=True, null=True)
user = ForeignKey(User, editable=False, blank=True, null=True, on_delete=models.SET_NULL) person = ForeignKey(Person, editable=False, blank=True, null=True, on_delete=models.SET_NULL)
time = models.DateTimeField(auto_now_add=True) time = models.DateTimeField(auto_now_add=True)
objects = FeedbackManager() objects = FeedbackManager()

View file

@ -115,11 +115,11 @@ class NomineePositionResource(ModelResource):
api.nomcom.register(NomineePositionResource()) api.nomcom.register(NomineePositionResource())
from ietf.name.resources import FeedbackTypeNameResource from ietf.name.resources import FeedbackTypeNameResource
from ietf.utils.resources import UserResource from ietf.person.resources import PersonResource
class FeedbackResource(ModelResource): class FeedbackResource(ModelResource):
nomcom = ToOneField(NomComResource, 'nomcom') nomcom = ToOneField(NomComResource, 'nomcom')
type = ToOneField(FeedbackTypeNameResource, 'type', null=True) type = ToOneField(FeedbackTypeNameResource, 'type', null=True)
user = ToOneField(UserResource, 'user', null=True) person = ToOneField(PersonResource, 'person', null=True)
positions = ToManyField(PositionResource, 'positions', null=True) positions = ToManyField(PositionResource, 'positions', null=True)
nominees = ToManyField(NomineeResource, 'nominees', null=True) nominees = ToManyField(NomineeResource, 'nominees', null=True)
class Meta: class Meta:
@ -136,18 +136,18 @@ class FeedbackResource(ModelResource):
"time": ALL, "time": ALL,
"nomcom": ALL_WITH_RELATIONS, "nomcom": ALL_WITH_RELATIONS,
"type": ALL_WITH_RELATIONS, "type": ALL_WITH_RELATIONS,
"user": ALL_WITH_RELATIONS, "person": ALL_WITH_RELATIONS,
"positions": ALL_WITH_RELATIONS, "positions": ALL_WITH_RELATIONS,
"nominees": ALL_WITH_RELATIONS, "nominees": ALL_WITH_RELATIONS,
} }
api.nomcom.register(FeedbackResource()) api.nomcom.register(FeedbackResource())
from ietf.utils.resources import UserResource from ietf.person.resources import PersonResource
class NominationResource(ModelResource): class NominationResource(ModelResource):
position = ToOneField(PositionResource, 'position') position = ToOneField(PositionResource, 'position')
nominee = ToOneField(NomineeResource, 'nominee') nominee = ToOneField(NomineeResource, 'nominee')
comments = ToOneField(FeedbackResource, 'comments') comments = ToOneField(FeedbackResource, 'comments')
user = ToOneField(UserResource, 'user', null=True) person = ToOneField(PersonResource, 'person', null=True)
class Meta: class Meta:
cache = SimpleCache() cache = SimpleCache()
queryset = Nomination.objects.all() queryset = Nomination.objects.all()
@ -164,7 +164,7 @@ class NominationResource(ModelResource):
"position": ALL_WITH_RELATIONS, "position": ALL_WITH_RELATIONS,
"nominee": ALL_WITH_RELATIONS, "nominee": ALL_WITH_RELATIONS,
"comments": ALL_WITH_RELATIONS, "comments": ALL_WITH_RELATIONS,
"user": ALL_WITH_RELATIONS, "person": ALL_WITH_RELATIONS,
} }
api.nomcom.register(NominationResource()) api.nomcom.register(NominationResource())

View file

@ -689,20 +689,16 @@ class NomcomViewsTest(TestCase):
self.assertIn('nominee@', outbox[1]['To']) self.assertIn('nominee@', outbox[1]['To'])
def nominate_view(self, *args, **kwargs): def nominate_view(self, public=True, searched_email=None,
public = kwargs.pop('public', True) nominee_email='nominee@example.com',
searched_email = kwargs.pop('searched_email', None) nominator_email=COMMUNITY_USER+EMAIL_DOMAIN,
nominee_email = kwargs.pop('nominee_email', 'nominee@example.com') position='IAOC', confirmation=False):
if not searched_email: if not searched_email:
searched_email = Email.objects.filter(address=nominee_email).first() searched_email = Email.objects.filter(address=nominee_email).first() or EmailFactory(address=nominee_email, primary=True, origin='test')
if not searched_email:
searched_email = EmailFactory(address=nominee_email, primary=True, origin='test')
if not searched_email.person: if not searched_email.person:
searched_email.person = PersonFactory() searched_email.person = PersonFactory()
searched_email.save() 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)
if public: if public:
nominate_url = self.public_nominate_url nominate_url = self.public_nominate_url
@ -726,7 +722,7 @@ class NomcomViewsTest(TestCase):
q = PyQuery(response.content) q = PyQuery(response.content)
self.assertEqual(len(q("#nominate-form")), 1) self.assertEqual(len(q("#nominate-form")), 1)
position = Position.objects.get(name=position_name) position = Position.objects.get(name=position)
comment_text = 'Test nominate view. Comments with accents äöåÄÖÅ éáíóú âêîôû ü àèìòù.' comment_text = 'Test nominate view. Comments with accents äöåÄÖÅ éáíóú âêîôû ü àèìòù.'
candidate_phone = '123456' candidate_phone = '123456'
@ -764,12 +760,9 @@ class NomcomViewsTest(TestCase):
comments=feedback, comments=feedback,
nominator_email="%s%s" % (COMMUNITY_USER, EMAIL_DOMAIN)) nominator_email="%s%s" % (COMMUNITY_USER, EMAIL_DOMAIN))
def nominate_newperson_view(self, *args, **kwargs): def nominate_newperson_view(self, public=True, nominee_email='nominee@example.com',
public = kwargs.pop('public', True) nominator_email=COMMUNITY_USER+EMAIL_DOMAIN,
nominee_email = kwargs.pop('nominee_email', 'nominee@example.com') position='IAOC', confirmation=False):
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: if public:
nominate_url = self.public_nominate_newperson_url nominate_url = self.public_nominate_newperson_url
@ -793,7 +786,7 @@ class NomcomViewsTest(TestCase):
q = PyQuery(response.content) q = PyQuery(response.content)
self.assertEqual(len(q("#nominate-form")), 1) self.assertEqual(len(q("#nominate-form")), 1)
position = Position.objects.get(name=position_name) position = Position.objects.get(name=position)
candidate_email = nominee_email candidate_email = nominee_email
candidate_name = 'nominee' candidate_name = 'nominee'
comment_text = 'Test nominate view. Comments with accents äöåÄÖÅ éáíóú âêîôû ü àèìòù.' comment_text = 'Test nominate view. Comments with accents äöåÄÖÅ éáíóú âêîôû ü àèìòù.'
@ -847,15 +840,13 @@ class NomcomViewsTest(TestCase):
self.access_chair_url(self.add_questionnaire_url) self.access_chair_url(self.add_questionnaire_url)
self.add_questionnaire() self.add_questionnaire()
def add_questionnaire(self, *args, **kwargs): def add_questionnaire(self, public=False, nominee_email='nominee@example.com',
public = kwargs.pop('public', False) nominator_email=COMMUNITY_USER+EMAIL_DOMAIN,
nominee_email = kwargs.pop('nominee_email', 'nominee@example.com') position='IAOC'):
nominator_email = kwargs.pop('nominator_email', "%s%s" % (COMMUNITY_USER, EMAIL_DOMAIN))
position_name = kwargs.pop('position', 'IAOC')
self.nominate_view(public=public, self.nominate_view(public=public,
nominee_email=nominee_email, nominee_email=nominee_email,
position=position_name, position=position,
nominator_email=nominator_email) nominator_email=nominator_email)
response = self.client.get(self.add_questionnaire_url) response = self.client.get(self.add_questionnaire_url)
@ -874,7 +865,7 @@ class NomcomViewsTest(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "questionnnaireform") self.assertContains(response, "questionnnaireform")
position = Position.objects.get(name=position_name) position = Position.objects.get(name=position)
nominee = Nominee.objects.get(email__address=nominee_email) nominee = Nominee.objects.get(email__address=nominee_email)
comment_text = 'Test add questionnaire view. Comments with accents äöåÄÖÅ éáíóú âêîôû ü àèìòù.' comment_text = 'Test add questionnaire view. Comments with accents äöåÄÖÅ éáíóú âêîôû ü àèìòù.'
@ -924,16 +915,13 @@ class NomcomViewsTest(TestCase):
self.access_member_url(self.private_feedback_url) self.access_member_url(self.private_feedback_url)
self.feedback_view(public=False) self.feedback_view(public=False)
def feedback_view(self, *args, **kwargs): def feedback_view(self, public=True, nominee_email='nominee@example.com',
public = kwargs.pop('public', True) nominator_email=COMMUNITY_USER+EMAIL_DOMAIN,
nominee_email = kwargs.pop('nominee_email', 'nominee@example.com') position='IAOC', confirmation=False):
nominator_email = kwargs.pop('nominator_email', "%s%s" % (COMMUNITY_USER, EMAIL_DOMAIN))
position_name = kwargs.pop('position', 'IAOC')
confirmation = kwargs.pop('confirmation', False)
self.nominate_view(public=public, self.nominate_view(public=public,
nominee_email=nominee_email, nominee_email=nominee_email,
position=position_name, position=position,
nominator_email=nominator_email) nominator_email=nominator_email)
feedback_url = self.public_feedback_url feedback_url = self.public_feedback_url
@ -956,7 +944,7 @@ class NomcomViewsTest(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertNotContains(response, "feedbackform") self.assertNotContains(response, "feedbackform")
position = Position.objects.get(name=position_name) position = Position.objects.get(name=position)
nominee = Nominee.objects.get(email__address=nominee_email) nominee = Nominee.objects.get(email__address=nominee_email)
feedback_url += "?nominee=%d&position=%d" % (nominee.id, position.id) feedback_url += "?nominee=%d&position=%d" % (nominee.id, position.id)
@ -972,7 +960,7 @@ class NomcomViewsTest(TestCase):
comments = 'Test feedback view. Comments with accents äöåÄÖÅ éáíóú âêîôû ü àèìòù.' comments = 'Test feedback view. Comments with accents äöåÄÖÅ éáíóú âêîôû ü àèìòù.'
test_data = {'comment_text': comments, test_data = {'comment_text': comments,
'position_name': position.name, 'position': position.name,
'nominee_name': nominee.email.person.name, 'nominee_name': nominee.email.person.name,
'nominee_email': nominee.email.address, 'nominee_email': nominee.email.address,
'confirmation': confirmation} 'confirmation': confirmation}
@ -1168,7 +1156,7 @@ class ReminderTest(TestCase):
feedback = Feedback.objects.create(nomcom=self.nomcom, feedback = Feedback.objects.create(nomcom=self.nomcom,
comments=self.nomcom.encrypt('some non-empty comments'), comments=self.nomcom.encrypt('some non-empty comments'),
type=FeedbackTypeName.objects.get(slug='questio'), type=FeedbackTypeName.objects.get(slug='questio'),
user=User.objects.get(username=CHAIR_USER)) person=User.objects.get(username=CHAIR_USER).person)
feedback.positions.add(gen) feedback.positions.add(gen)
feedback.nominees.add(n) feedback.nominees.add(n)
@ -2192,7 +2180,7 @@ class AcceptingTests(TestCase):
self.assertIn('not currently accepting feedback', unicontent(response)) self.assertIn('not currently accepting feedback', unicontent(response))
test_data = {'comment_text': 'junk', test_data = {'comment_text': 'junk',
'position_name': pos.name, 'position': pos.name,
'nominee_name': pos.nominee_set.first().email.person.name, 'nominee_name': pos.nominee_set.first().email.person.name,
'nominee_email': pos.nominee_set.first().email.address, 'nominee_email': pos.nominee_set.first().email.address,
'confirmation': False, 'confirmation': False,

View file

@ -88,26 +88,21 @@ def get_year_by_nomcom(nomcom):
return m.group(0) return m.group(0)
def get_user_email(user): def get_person_email(person):
# a user object already has an email field, but we don't want to if not hasattr(person, "_email_cache"):
# overwrite anything that might be there, and we don't know that person._email_cache = None
# what's there is the right thing, so we cache the lookup results in a emails = person.email_set.filter(active=True).order_by('-time')
# separate attribute if emails:
if not hasattr(user, "_email_cache"): person._email_cache = emails[0]
user._email_cache = None for email in emails:
if hasattr(user, "person"): if email.address.lower() == person.user.username.lower():
emails = user.person.email_set.filter(active=True).order_by('-time') person._email_cache = email
if emails:
user._email_cache = emails[0]
for email in emails:
if email.address.lower() == user.username.lower():
user._email_cache = email
else: else:
try: try:
user._email_cache = Email.objects.get(address=user.username) person._email_cache = Email.objects.get(address=person.user.username)
except ObjectDoesNotExist: except ObjectDoesNotExist:
pass pass
return user._email_cache return person._email_cache
def get_hash_nominee_position(date, nominee_position_id): def get_hash_nominee_position(date, nominee_position_id):
return hmac.new(settings.NOMCOM_APP_SECRET, f"{date}{nominee_position_id}".encode('utf-8'), hashlib.sha256).hexdigest() return hmac.new(settings.NOMCOM_APP_SECRET, f"{date}{nominee_position_id}".encode('utf-8'), hashlib.sha256).hexdigest()

View file

@ -454,23 +454,24 @@ def nominate(request, year, public, newperson):
{'nomcom': nomcom, {'nomcom': nomcom,
'year': year}) 'year': year})
person = request.user.person
if request.method == 'POST': if request.method == 'POST':
if newperson: if newperson:
form = NominateNewPersonForm(data=request.POST, nomcom=nomcom, user=request.user, public=public) form = NominateNewPersonForm(data=request.POST, nomcom=nomcom, person=person, public=public)
else: else:
form = NominateForm(data=request.POST, nomcom=nomcom, user=request.user, public=public) form = NominateForm(data=request.POST, nomcom=nomcom, person=person, public=public)
if form.is_valid(): if form.is_valid():
form.save() form.save()
messages.success(request, '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: if newperson:
return redirect('ietf.nomcom.views.%s_nominate' % ('public' if public else 'private'), year=year) return redirect('ietf.nomcom.views.%s_nominate' % ('public' if public else 'private'), year=year)
else: else:
form = NominateForm(nomcom=nomcom, user=request.user, public=public) form = NominateForm(nomcom=nomcom, person=person, public=public)
else: else:
if newperson: if newperson:
form = NominateNewPersonForm(nomcom=nomcom, user=request.user, public=public) form = NominateNewPersonForm(nomcom=nomcom, person=person, public=public)
else: else:
form = NominateForm(nomcom=nomcom, user=request.user, public=public) form = NominateForm(nomcom=nomcom, person=person, public=public)
return render(request, template, return render(request, template,
{'form': form, {'form': form,
@ -494,6 +495,7 @@ def feedback(request, year, public):
nominee = None nominee = None
position = None position = None
topic = None topic = None
person = request.user.person
if nomcom.group.state_id != 'conclude': if nomcom.group.state_id != 'conclude':
selected_nominee = request.GET.get('nominee') selected_nominee = request.GET.get('nominee')
selected_position = request.GET.get('position') selected_position = request.GET.get('position')
@ -505,7 +507,7 @@ def feedback(request, year, public):
topic = get_object_or_404(Topic,id=selected_topic) topic = get_object_or_404(Topic,id=selected_topic)
if topic.audience_id == 'nomcom' and not nomcom.group.has_role(request.user, ['chair','advisor','liaison','member']): if topic.audience_id == 'nomcom' and not nomcom.group.has_role(request.user, ['chair','advisor','liaison','member']):
raise Http404() raise Http404()
if topic.audience_id == 'nominees' and not nomcom.nominee_set.filter(person=request.user.person).exists(): if topic.audience_id == 'nominees' and not nomcom.nominee_set.filter(person=person).exists():
raise Http404() raise Http404()
if public: if public:
@ -517,12 +519,12 @@ def feedback(request, year, public):
if not nomcom.group.has_role(request.user, ['chair','advisor','liaison','member']): if not nomcom.group.has_role(request.user, ['chair','advisor','liaison','member']):
topics = topics.exclude(audience_id='nomcom') topics = topics.exclude(audience_id='nomcom')
if not nomcom.nominee_set.filter(person=request.user.person).exists(): if not nomcom.nominee_set.filter(person=person).exists():
topics = topics.exclude(audience_id='nominees') topics = topics.exclude(audience_id='nominees')
user_comments = Feedback.objects.filter(nomcom=nomcom, user_comments = Feedback.objects.filter(nomcom=nomcom,
type='comment', type='comment',
author__in=request.user.person.email_set.filter(active='True')) author__in=person.email_set.filter(active='True'))
counter = Counter(user_comments.values_list('positions','nominees')) counter = Counter(user_comments.values_list('positions','nominees'))
counts = dict() counts = dict()
for pos,nom in counter: for pos,nom in counter:
@ -572,11 +574,11 @@ def feedback(request, year, public):
if request.method == 'POST': if request.method == 'POST':
if nominee and position: if nominee and position:
form = FeedbackForm(data=request.POST, form = FeedbackForm(data=request.POST,
nomcom=nomcom, user=request.user, nomcom=nomcom, person=person,
public=public, position=position, nominee=nominee) public=public, position=position, nominee=nominee)
elif topic: elif topic:
form = FeedbackForm(data=request.POST, form = FeedbackForm(data=request.POST,
nomcom=nomcom, user=request.user, nomcom=nomcom, person=person,
public=public, topic=topic) public=public, topic=topic)
else: else:
form = None form = None
@ -595,10 +597,10 @@ def feedback(request, year, public):
pass pass
else: else:
if nominee and position: if nominee and position:
form = FeedbackForm(nomcom=nomcom, user=request.user, public=public, form = FeedbackForm(nomcom=nomcom, person=person, public=public,
position=position, nominee=nominee) position=position, nominee=nominee)
elif topic: elif topic:
form = FeedbackForm(nomcom=nomcom, user=request.user, public=public, form = FeedbackForm(nomcom=nomcom, person=person, public=public,
topic=topic) topic=topic)
else: else:
form = None form = None
@ -661,6 +663,7 @@ def private_questionnaire(request, year):
has_publickey = nomcom.public_key and True or False has_publickey = nomcom.public_key and True or False
questionnaire_response = None questionnaire_response = None
template = 'nomcom/private_questionnaire.html' template = 'nomcom/private_questionnaire.html'
person = request.user.person
if not has_publickey: if not has_publickey:
messages.warning(request, "This Nomcom is not yet accepting questionnaires.") messages.warning(request, "This Nomcom is not yet accepting questionnaires.")
@ -680,14 +683,14 @@ def private_questionnaire(request, year):
if request.method == 'POST': if request.method == 'POST':
form = QuestionnaireForm(data=request.POST, form = QuestionnaireForm(data=request.POST,
nomcom=nomcom, user=request.user) nomcom=nomcom, person=person)
if form.is_valid(): if form.is_valid():
form.save() form.save()
messages.success(request, 'The questionnaire response has been registered.') messages.success(request, 'The questionnaire response has been registered.')
questionnaire_response = force_str(form.cleaned_data['comment_text']) questionnaire_response = force_str(form.cleaned_data['comment_text'])
form = QuestionnaireForm(nomcom=nomcom, user=request.user) form = QuestionnaireForm(nomcom=nomcom, person=person)
else: else:
form = QuestionnaireForm(nomcom=nomcom, user=request.user) form = QuestionnaireForm(nomcom=nomcom, person=person)
return render(request, template, return render(request, template,
{'form': form, {'form': form,
@ -725,15 +728,13 @@ def process_nomination_status(request, year, nominee_position_id, state, date, h
if form.cleaned_data['comments']: if form.cleaned_data['comments']:
# This Feedback object is of type comment instead of nomina in order to not # This Feedback object is of type comment instead of nomina in order to not
# make answering "who nominated themselves" harder. # make answering "who nominated themselves" harder.
who = request.user who = None if isinstance(request.user, AnonymousUser) else request.user.person
if isinstance(who,AnonymousUser):
who = None
f = Feedback.objects.create(nomcom = nomcom, f = Feedback.objects.create(nomcom = nomcom,
author = nominee_position.nominee.email, author = nominee_position.nominee.email,
subject = '%s nomination %s'%(nominee_position.nominee.name(),state), subject = '%s nomination %s'%(nominee_position.nominee.name(),state),
comments = nomcom.encrypt(form.cleaned_data['comments']), comments = nomcom.encrypt(form.cleaned_data['comments']),
type_id = 'comment', type_id = 'comment',
user = who, person = who,
) )
f.positions.add(nominee_position.position) f.positions.add(nominee_position.position)
f.nominees.add(nominee_position.nominee) f.nominees.add(nominee_position.nominee)
@ -779,8 +780,9 @@ def view_feedback(request, year):
sorted_nominees = sorted(nominees,key=lambda x:x.staterank) sorted_nominees = sorted(nominees,key=lambda x:x.staterank)
reviewer = request.user.person
for nominee in sorted_nominees: for nominee in sorted_nominees:
last_seen = FeedbackLastSeen.objects.filter(reviewer=request.user.person,nominee=nominee).first() last_seen = FeedbackLastSeen.objects.filter(reviewer=reviewer,nominee=nominee).first()
nominee_feedback = [] nominee_feedback = []
for ft in nominee_feedback_types: for ft in nominee_feedback_types:
qs = nominee.feedback_set.by_type(ft.slug) qs = nominee.feedback_set.by_type(ft.slug)
@ -795,7 +797,7 @@ def view_feedback(request, year):
nominees_feedback.append( {'nominee':nominee, 'feedback':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] independent_feedback = [ft.feedback_set.get_by_nomcom(nomcom).count() for ft in independent_feedback_types]
for topic in nomcom.topic_set.all(): for topic in nomcom.topic_set.all():
last_seen = TopicFeedbackLastSeen.objects.filter(reviewer=request.user.person,topic=topic).first() last_seen = TopicFeedbackLastSeen.objects.filter(reviewer=reviewer,topic=topic).first()
topic_feedback = [] topic_feedback = []
for ft in topic_feedback_types: for ft in topic_feedback_types:
qs = topic.feedback_set.by_type(ft.slug) qs = topic.feedback_set.by_type(ft.slug)
@ -842,6 +844,7 @@ def view_feedback_pending(request, year):
except EmptyPage: except EmptyPage:
feedback_page = paginator.page(paginator.num_pages) feedback_page = paginator.page(paginator.num_pages)
extra_step = False extra_step = False
person = request.user.person
if 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_ids = request.POST.get('extra_ids', None)
extra_step = True extra_step = True
@ -850,7 +853,7 @@ def view_feedback_pending(request, year):
formset.absolute_max = 2000 formset.absolute_max = 2000
formset.validate_max = False formset.validate_max = False
for form in formset.forms: for form in formset.forms:
form.set_nomcom(nomcom, request.user) form.set_nomcom(nomcom, person)
if formset.is_valid(): if formset.is_valid():
formset.save() formset.save()
if extra_ids: if extra_ids:
@ -862,7 +865,7 @@ def view_feedback_pending(request, year):
extra.append(feedback) extra.append(feedback)
formset = FullFeedbackFormSet(queryset=Feedback.objects.filter(id__in=[i.id for i in extra])) formset = FullFeedbackFormSet(queryset=Feedback.objects.filter(id__in=[i.id for i in extra]))
for form in formset.forms: for form in formset.forms:
form.set_nomcom(nomcom, request.user, extra) form.set_nomcom(nomcom, person, extra)
extra_ids = None extra_ids = None
else: else:
messages.success(request, 'Feedback saved') messages.success(request, 'Feedback saved')
@ -870,7 +873,7 @@ def view_feedback_pending(request, year):
elif request.method == 'POST': elif request.method == 'POST':
formset = FeedbackFormSet(request.POST) formset = FeedbackFormSet(request.POST)
for form in formset.forms: for form in formset.forms:
form.set_nomcom(nomcom, request.user) form.set_nomcom(nomcom, person)
if formset.is_valid(): if formset.is_valid():
extra = [] extra = []
nominations = [] nominations = []
@ -890,12 +893,12 @@ def view_feedback_pending(request, year):
if nominations: if nominations:
formset = FullFeedbackFormSet(queryset=Feedback.objects.filter(id__in=[i.id for i in nominations])) formset = FullFeedbackFormSet(queryset=Feedback.objects.filter(id__in=[i.id for i in nominations]))
for form in formset.forms: for form in formset.forms:
form.set_nomcom(nomcom, request.user, nominations) form.set_nomcom(nomcom, person, nominations)
extra_ids = ','.join(['%s:%s' % (i.id, i.type.pk) for i in extra]) extra_ids = ','.join(['%s:%s' % (i.id, i.type.pk) for i in extra])
else: else:
formset = FullFeedbackFormSet(queryset=Feedback.objects.filter(id__in=[i.id for i in extra])) formset = FullFeedbackFormSet(queryset=Feedback.objects.filter(id__in=[i.id for i in extra]))
for form in formset.forms: for form in formset.forms:
form.set_nomcom(nomcom, request.user, extra) form.set_nomcom(nomcom, person, extra)
if moved: if moved:
messages.success(request, '%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: else:
@ -904,7 +907,7 @@ def view_feedback_pending(request, year):
else: else:
formset = FeedbackFormSet(queryset=feedback_page.object_list) formset = FeedbackFormSet(queryset=feedback_page.object_list)
for form in formset.forms: for form in formset.forms:
form.set_nomcom(nomcom, request.user) form.set_nomcom(nomcom, person)
return render(request, 'nomcom/view_feedback_pending.html', return render(request, 'nomcom/view_feedback_pending.html',
{'year': year, {'year': year,
'formset': formset, 'formset': formset,
@ -975,13 +978,14 @@ def view_feedback_topic(request, year, topic_id):
topic = get_object_or_404(Topic, id=topic_id) topic = get_object_or_404(Topic, id=topic_id)
nomcom = get_nomcom_by_year(year) nomcom = get_nomcom_by_year(year)
feedback_types = FeedbackTypeName.objects.filter(slug__in=['comment',]) feedback_types = FeedbackTypeName.objects.filter(slug__in=['comment',])
reviewer = request.user.person
last_seen = TopicFeedbackLastSeen.objects.filter(reviewer=request.user.person,topic=topic).first() last_seen = TopicFeedbackLastSeen.objects.filter(reviewer=reviewer,topic=topic).first()
last_seen_time = (last_seen and last_seen.time) or datetime.datetime(year=1, month=1, day=1, tzinfo=datetime.timezone.utc) last_seen_time = (last_seen and last_seen.time) or datetime.datetime(year=1, month=1, day=1, tzinfo=datetime.timezone.utc)
if last_seen: if last_seen:
last_seen.save() last_seen.save()
else: else:
TopicFeedbackLastSeen.objects.create(reviewer=request.user.person,topic=topic) TopicFeedbackLastSeen.objects.create(reviewer=reviewer,topic=topic)
return render(request, 'nomcom/view_feedback_topic.html', return render(request, 'nomcom/view_feedback_topic.html',
{'year': year, {'year': year,
@ -997,7 +1001,7 @@ def view_feedback_nominee(request, year, nominee_id):
nomcom = get_nomcom_by_year(year) nomcom = get_nomcom_by_year(year)
nominee = get_object_or_404(Nominee, id=nominee_id) nominee = get_object_or_404(Nominee, id=nominee_id)
feedback_types = FeedbackTypeName.objects.filter(used=True, slug__in=settings.NOMINEE_FEEDBACK_TYPES) feedback_types = FeedbackTypeName.objects.filter(used=True, slug__in=settings.NOMINEE_FEEDBACK_TYPES)
reviewer = request.user.person
if request.method == 'POST': if request.method == 'POST':
if not nomcom.group.has_role(request.user, ['chair','advisor']): if not nomcom.group.has_role(request.user, ['chair','advisor']):
return HttpResponseForbidden('Restricted to roles: Nomcom Chair, Nomcom Advisor') return HttpResponseForbidden('Restricted to roles: Nomcom Chair, Nomcom Advisor')
@ -1036,12 +1040,12 @@ def view_feedback_nominee(request, year, nominee_id):
'is_chair_task': True, 'is_chair_task': True,
}) })
last_seen = FeedbackLastSeen.objects.filter(reviewer=request.user.person,nominee=nominee).first() last_seen = FeedbackLastSeen.objects.filter(reviewer=reviewer,nominee=nominee).first()
last_seen_time = (last_seen and last_seen.time) or datetime.datetime(year=1, month=1, day=1, tzinfo=datetime.timezone.utc) last_seen_time = (last_seen and last_seen.time) or datetime.datetime(year=1, month=1, day=1, tzinfo=datetime.timezone.utc)
if last_seen: if last_seen:
last_seen.save() last_seen.save()
else: else:
FeedbackLastSeen.objects.create(reviewer=request.user.person,nominee=nominee) FeedbackLastSeen.objects.create(reviewer=reviewer,nominee=nominee)
return render(request, 'nomcom/view_feedback_nominee.html', return render(request, 'nomcom/view_feedback_nominee.html',
{'year': year, {'year': year,
@ -1322,15 +1326,15 @@ def configuration_help(request, year):
@role_required("Nomcom Chair", "Nomcom Advisor") @role_required("Nomcom Chair", "Nomcom Advisor")
def edit_members(request, year): def edit_members(request, year):
nomcom = get_nomcom_by_year(year) nomcom = get_nomcom_by_year(year)
if nomcom.group.state_id=='conclude': if nomcom.group.state_id=='conclude':
permission_denied(request, 'This nomcom is closed.') permission_denied(request, 'This nomcom is closed.')
person = request.user.person
if request.method=='POST': if request.method=='POST':
form = EditMembersForm(nomcom, data=request.POST) form = EditMembersForm(nomcom, data=request.POST)
if form.is_valid(): if form.is_valid():
update_role_set(nomcom.group, 'member', form.cleaned_data['members'], request.user.person) update_role_set(nomcom.group, 'member', form.cleaned_data['members'], person)
update_role_set(nomcom.group, 'liaison', form.cleaned_data['liaisons'], request.user.person) update_role_set(nomcom.group, 'liaison', form.cleaned_data['liaisons'], person)
return HttpResponseRedirect(reverse('ietf.nomcom.views.private_index',kwargs={'year':year})) return HttpResponseRedirect(reverse('ietf.nomcom.views.private_index',kwargs={'year':year}))
else: else:
form = EditMembersForm(nomcom) form = EditMembersForm(nomcom)

View file

@ -25,11 +25,11 @@ from ietf.group.models import Group
from ietf.nomcom.models import NomCom from ietf.nomcom.models import NomCom
from ietf.nomcom.test_data import nomcom_test_data from ietf.nomcom.test_data import nomcom_test_data
from ietf.nomcom.factories import NomComFactory, NomineeFactory, NominationFactory, FeedbackFactory, PositionFactory from ietf.nomcom.factories import NomComFactory, NomineeFactory, NominationFactory, FeedbackFactory, PositionFactory
from ietf.person.factories import EmailFactory, PersonFactory, UserFactory from ietf.person.factories import EmailFactory, PersonFactory
from ietf.person.models import Person, Alias from ietf.person.models import Person, Alias
from ietf.person.utils import (merge_persons, determine_merge_order, send_merge_notification, from ietf.person.utils import (merge_persons, determine_merge_order, send_merge_notification,
handle_users, get_extra_primary, dedupe_aliases, move_related_objects, merge_nominees, handle_users, get_extra_primary, dedupe_aliases, move_related_objects, merge_nominees,
handle_reviewer_settings, merge_users, get_dots) handle_reviewer_settings, get_dots)
from ietf.review.models import ReviewerSettings from ietf.review.models import ReviewerSettings
from ietf.utils.test_utils import TestCase, login_testing_unauthorized from ietf.utils.test_utils import TestCase, login_testing_unauthorized
from ietf.utils.mail import outbox, empty_outbox from ietf.utils.mail import outbox, empty_outbox
@ -165,6 +165,16 @@ class PersonTests(TestCase):
img = Image.open(BytesIO(r.content)) img = Image.open(BytesIO(r.content))
self.assertEqual(img.width, 200) self.assertEqual(img.width, 200)
def test_person_photo_duplicates(self):
person = PersonFactory(name="bazquux@example.com", user__username="bazquux@example.com", with_bio=True)
PersonFactory(name="bazquux@example.com", user__username="foobar@example.com", with_bio=True)
url = urlreverse("ietf.person.views.photo", kwargs={ "email_or_name": person.plain_name()})
r = self.client.get(url)
self.assertEqual(r.status_code, 300)
self.assertIn("bazquux@example.com", r.content.decode())
self.assertIn("foobar@example.com", r.content.decode())
def test_name_methods(self): def test_name_methods(self):
person = PersonFactory(name="Dr. Jens F. Möller", ) person = PersonFactory(name="Dr. Jens F. Möller", )
@ -381,13 +391,24 @@ class PersonUtilsTests(TestCase):
request.user = user request.user = user
source = PersonFactory() source = PersonFactory()
target = PersonFactory() target = PersonFactory()
mars = RoleFactory(name_id='chair',group__acronym='mars').group
source_id = source.pk source_id = source.pk
source_email = source.email_set.first() source_email = source.email_set.first()
source_alias = source.alias_set.first() source_alias = source.alias_set.first()
source_user = source.user source_user = source.user
communitylist = CommunityList.objects.create(person=source, group=mars)
nomcom = NomComFactory()
position = PositionFactory(nomcom=nomcom)
nominee = NomineeFactory(nomcom=nomcom, person=mars.get_chair().person)
feedback = FeedbackFactory(person=source, author=source.email().address, nomcom=nomcom)
feedback.nominees.add(nominee)
nomination = NominationFactory(nominee=nominee, person=source, position=position, comments=feedback)
merge_persons(request, source, target, file=StringIO()) merge_persons(request, source, target, file=StringIO())
self.assertTrue(source_email in target.email_set.all()) self.assertTrue(source_email in target.email_set.all())
self.assertTrue(source_alias in target.alias_set.all()) self.assertTrue(source_alias in target.alias_set.all())
self.assertIn(communitylist, target.communitylist_set.all())
self.assertIn(feedback, target.feedback_set.all())
self.assertIn(nomination, target.nomination_set.all())
self.assertFalse(Person.objects.filter(id=source_id)) self.assertFalse(Person.objects.filter(id=source_id))
self.assertFalse(source_user.is_active) self.assertFalse(source_user.is_active)
@ -407,24 +428,6 @@ class PersonUtilsTests(TestCase):
rs = target.reviewersettings_set.first() rs = target.reviewersettings_set.first()
self.assertEqual(rs.min_interval, 7) self.assertEqual(rs.min_interval, 7)
def test_merge_users(self):
person = PersonFactory()
source = person.user
target = UserFactory()
mars = RoleFactory(name_id='chair',group__acronym='mars').group
communitylist = CommunityList.objects.create(user=source, group=mars)
nomcom = NomComFactory()
position = PositionFactory(nomcom=nomcom)
nominee = NomineeFactory(nomcom=nomcom, person=mars.get_chair().person)
feedback = FeedbackFactory(user=source, author=person.email().address, nomcom=nomcom)
feedback.nominees.add(nominee)
nomination = NominationFactory(nominee=nominee, user=source, position=position, comments=feedback)
merge_users(source, target)
self.assertIn(communitylist, target.communitylist_set.all())
self.assertIn(feedback, target.feedback_set.all())
self.assertIn(nomination, target.nomination_set.all())
def test_dots(self): def test_dots(self):
noroles = PersonFactory() noroles = PersonFactory()
self.assertEqual(get_dots(noroles),[]) self.assertEqual(get_dots(noroles),[])

View file

@ -12,10 +12,11 @@ from django.contrib import admin
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q from django.db.models import Q
from django.http import Http404
import debug # pyflakes:ignore import debug # pyflakes:ignore
from ietf.person.models import Person from ietf.person.models import Person, Alias, Email
from ietf.utils.mail import send_mail from ietf.utils.mail import send_mail
def merge_persons(request, source, target, file=sys.stdout, verbose=False): def merge_persons(request, source, target, file=sys.stdout, verbose=False):
@ -31,6 +32,20 @@ def merge_persons(request, source, target, file=sys.stdout, verbose=False):
email.save() email.save()
changes.append('EMAIL ACTION: {} no longer marked as primary'.format(email.address)) changes.append('EMAIL ACTION: {} no longer marked as primary'.format(email.address))
# handle community list
for communitylist in source.communitylist_set.all():
source.communitylist_set.remove(communitylist)
target.communitylist_set.add(communitylist)
# handle feedback
for feedback in source.feedback_set.all():
feedback.person = target
feedback.save()
# handle nominations
for nomination in source.nomination_set.all():
nomination.person = target
nomination.save()
changes.append(handle_users(source, target)) changes.append(handle_users(source, target))
reviewer_changes = handle_reviewer_settings(source, target) reviewer_changes = handle_reviewer_settings(source, target)
if reviewer_changes: if reviewer_changes:
@ -103,7 +118,6 @@ def handle_users(source,target,check_only=False):
if source.user and target.user: if source.user and target.user:
message = "DATATRACKER LOGIN ACTION: retaining login: {}, removing login: {}".format(target.user,source.user) message = "DATATRACKER LOGIN ACTION: retaining login: {}, removing login: {}".format(target.user,source.user)
if not check_only: if not check_only:
merge_users(source.user, target.user)
syslog.syslog('merge-person-records: deactivating user {}'.format(source.user.username)) syslog.syslog('merge-person-records: deactivating user {}'.format(source.user.username))
user = source.user user = source.user
source.user = None source.user = None
@ -126,21 +140,6 @@ def move_related_objects(source, target, file, verbose=False):
kwargs = { field_name:target } kwargs = { field_name:target }
queryset.update(**kwargs) queryset.update(**kwargs)
def merge_users(source, target):
'''Move related objects from source user to target user'''
# handle community list
for communitylist in source.communitylist_set.all():
source.communitylist_set.remove(communitylist)
target.communitylist_set.add(communitylist)
# handle feedback
for feedback in source.feedback_set.all():
feedback.user = target
feedback.save()
# handle nominations
for nomination in source.nomination_set.all():
nomination.user = target
nomination.save()
def dedupe_aliases(person): def dedupe_aliases(person):
'''Check person for duplicate aliases and purge''' '''Check person for duplicate aliases and purge'''
seen = [] seen = []
@ -248,3 +247,17 @@ def get_dots(person):
if roles.filter(group__acronym__startswith='nomcom', name_id__in=('chair','member')).exists(): if roles.filter(group__acronym__startswith='nomcom', name_id__in=('chair','member')).exists():
dots.append('nomcom') dots.append('nomcom')
return dots return dots
def lookup_persons(email_or_name):
aliases = Alias.objects.filter(name__iexact=email_or_name)
persons = set(a.person for a in aliases)
if '@' in email_or_name:
emails = Email.objects.filter(address__iexact=email_or_name)
persons.update(e.person for e in emails)
persons = [p for p in persons if p and p.id]
if not persons:
raise Http404
persons.sort(key=lambda p: p.id)
return persons

View file

@ -8,16 +8,16 @@ from PIL import Image
from django.contrib import messages from django.contrib import messages
from django.db.models import Q from django.db.models import Q
from django.http import HttpResponse, Http404 from django.http import HttpResponse, Http404
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, redirect
from django.utils import timezone from django.utils import timezone
import debug # pyflakes:ignore import debug # pyflakes:ignore
from ietf.ietfauth.utils import role_required from ietf.ietfauth.utils import role_required
from ietf.person.models import Email, Person, Alias from ietf.person.models import Email, Person
from ietf.person.fields import select2_id_name_json from ietf.person.fields import select2_id_name_json
from ietf.person.forms import MergeForm from ietf.person.forms import MergeForm
from ietf.person.utils import handle_users, merge_persons from ietf.person.utils import handle_users, merge_persons, lookup_persons
def ajax_select2_search(request, model_name): def ajax_select2_search(request, model_name):
@ -69,30 +69,14 @@ def ajax_select2_search(request, model_name):
def profile(request, email_or_name): def profile(request, email_or_name):
aliases = Alias.objects.filter(name__iexact=email_or_name) persons = lookup_persons(email_or_name)
persons = set(a.person for a in aliases)
if '@' in email_or_name:
emails = Email.objects.filter(address__iexact=email_or_name)
persons.update(e.person for e in emails)
persons = [p for p in persons if p and p.id]
if not persons:
raise Http404
persons.sort(key=lambda p: p.id)
return render(request, 'person/profile.html', {'persons': persons, 'today': timezone.now()}) return render(request, 'person/profile.html', {'persons': persons, 'today': timezone.now()})
def photo(request, email_or_name): def photo(request, email_or_name):
if '@' in email_or_name: persons = lookup_persons(email_or_name)
persons = [ get_object_or_404(Email, address=email_or_name).person, ]
else:
aliases = Alias.objects.filter(name=email_or_name)
persons = list(set([ a.person for a in aliases ]))
if not persons:
raise Http404("No such person")
if len(persons) > 1: if len(persons) > 1:
return HttpResponse(r"\r\n".join([p.email() for p in persons]), status=300) return HttpResponse(r"\r\n".join([p.user.username for p in persons]), status=300)
person = persons[0] person = persons[0]
if not person.photo: if not person.photo:
raise Http404("No photo found") raise Http404("No photo found")

View file

@ -7,7 +7,6 @@ from django.urls import reverse
from ietf.ietfauth.utils import role_required from ietf.ietfauth.utils import role_required
from ietf.person.models import Person, Email, Alias from ietf.person.models import Person, Email, Alias
from ietf.person.utils import merge_users
from ietf.secr.rolodex.forms import EditPersonForm, EmailForm, NameForm, NewPersonForm, SearchForm from ietf.secr.rolodex.forms import EditPersonForm, EmailForm, NameForm, NewPersonForm, SearchForm
@ -179,7 +178,6 @@ def edit(request, id):
if 'user' in person_form.changed_data and person_form.initial['user']: if 'user' in person_form.changed_data and person_form.initial['user']:
try: try:
source = User.objects.get(username__iexact=person_form.initial['user']) source = User.objects.get(username__iexact=person_form.initial['user'])
merge_users(source, person_form.cleaned_data['user'])
source.is_active = False source.is_active = False
source.save() source.save()
except User.DoesNotExist: except User.DoesNotExist:

View file

@ -3,18 +3,18 @@
<div class="btn-group" role="group" aria-labelledby="list-feeds"> <div class="btn-group" role="group" aria-labelledby="list-feeds">
<a class="btn btn-primary" <a class="btn btn-primary"
title="Feed of all changes" title="Feed of all changes"
href="{% if clist.group %}{% url "ietf.community.views.feed" acronym=clist.group.acronym %}{% else %}{% url "ietf.community.views.feed" username=clist.user.username %}{% endif %}"> href="{% if clist.group %}{% url "ietf.community.views.feed" acronym=clist.group.acronym %}{% else %}{% url "ietf.community.views.feed" email_or_name=clist.person.email_address %}{% endif %}">
<i class="bi bi-rss"></i> All changes <i class="bi bi-rss"></i> All changes
</a> </a>
<a class="btn btn-primary" <a class="btn btn-primary"
title="Feed of only significant state changes" title="Feed of only significant state changes"
href="{% if clist.group %}{% url "ietf.community.views.feed" acronym=clist.group.acronym %}{% else %}{% url "ietf.community.views.feed" username=clist.user.username %}{% endif %}?significant=1"> href="{% if clist.group %}{% url "ietf.community.views.feed" acronym=clist.group.acronym %}{% else %}{% url "ietf.community.views.feed" email_or_name=clist.person.email_address %}{% endif %}?significant=1">
<i class="bi bi-rss"></i> Significant <i class="bi bi-rss"></i> Significant
</a> </a>
</div> </div>
{% if clist.pk != None %} {% if clist.pk != None %}
<a class="btn btn-primary" <a class="btn btn-primary"
href="{% if clist.group %}{% url "ietf.community.views.subscription" acronym=clist.group.acronym %}{% else %}{% url "ietf.community.views.subscription" username=clist.user.username %}{% endif %}"> href="{% if clist.group %}{% url "ietf.community.views.subscription" acronym=clist.group.acronym %}{% else %}{% url "ietf.community.views.subscription" email_or_name=clist.person.email_address %}{% endif %}">
<i class="bi bi-envelope"></i> <i class="bi bi-envelope"></i>
{% if subscribed %} {% if subscribed %}
Change subscription Change subscription
@ -24,7 +24,7 @@
</a> </a>
{% endif %} {% endif %}
<a class="btn btn-primary" <a class="btn btn-primary"
href="{% if clist.group %}{% url "ietf.community.views.export_to_csv" acronym=clist.group.acronym %}{% else %}{% url "ietf.community.views.export_to_csv" username=clist.user.username %}{% endif %}"> href="{% if clist.group %}{% url "ietf.community.views.export_to_csv" acronym=clist.group.acronym %}{% else %}{% url "ietf.community.views.export_to_csv" email_or_name=clist.person.email_address %}{% endif %}">
<i class="bi bi-file-ruled"></i> Export as CSV <i class="bi bi-file-ruled"></i> Export as CSV
</a> </a>
</div> </div>

View file

@ -12,7 +12,7 @@
{% bootstrap_messages %} {% bootstrap_messages %}
{% if can_manage_list %} {% if can_manage_list %}
<a class="btn btn-primary my-3" <a class="btn btn-primary my-3"
href="{% url "ietf.community.views.manage_list" username=clist.user.username %}"> href="{% url "ietf.community.views.manage_list" email_or_name=clist.person.email_address %}">
<i class="bi bi-gear"></i> <i class="bi bi-gear"></i>
Manage list Manage list
</a> </a>
@ -22,4 +22,4 @@
{% endblock %} {% endblock %}
{% block js %} {% block js %}
<script src="{% static "ietf/js/list.js" %}"></script> <script src="{% static "ietf/js/list.js" %}"></script>
{% endblock %} {% endblock %}

View file

@ -699,21 +699,21 @@
</div> </div>
{% if user.is_authenticated %} {% if user.is_authenticated %}
<a class="btn btn-primary btn-sm track-untrack-doc {% if not doc.tracked_in_personal_community_list %}d-none{% endif %}" <a class="btn btn-primary btn-sm track-untrack-doc {% if not doc.tracked_in_personal_community_list %}d-none{% endif %}"
href="{% url "ietf.community.views.untrack_document" username=user.username name=doc.name %}" href="{% url "ietf.community.views.untrack_document" email_or_name=user.username name=doc.name %}"
title="Remove from your personal I-D list"> title="Remove from your personal I-D list">
<i class="bi bi-bookmark-check-fill"> <i class="bi bi-bookmark-check-fill">
</i> </i>
Untrack Untrack
</a> </a>
<a class="btn btn-primary btn-sm track-untrack-doc {% if doc.tracked_in_personal_community_list %}d-none{% endif %}" <a class="btn btn-primary btn-sm track-untrack-doc {% if doc.tracked_in_personal_community_list %}d-none{% endif %}"
href="{% url "ietf.community.views.track_document" username=user.username name=doc.name %}" href="{% url "ietf.community.views.track_document" email_or_name=user.username name=doc.name %}"
title="Add to your personal I-D list"> title="Add to your personal I-D list">
<i class="bi bi-bookmark"> <i class="bi bi-bookmark">
</i> </i>
Track Track
</a> </a>
{% endif %} {% endif %}
{% if user.review_teams %} {% if user.person.review_teams %}
<a class="btn btn-primary btn-sm review-wish-add-remove-doc ajax {% if not doc.has_review_wish %}d-none{% endif %}" <a class="btn btn-primary btn-sm review-wish-add-remove-doc ajax {% if not doc.has_review_wish %}d-none{% endif %}"
href="{% url "ietf.doc.views_review.review_wishes_remove" name=doc.name %}?next={{ request.get_full_path|urlencode }}" href="{% url "ietf.doc.views_review.review_wishes_remove" name=doc.name %}?next={{ request.get_full_path|urlencode }}"
title="Remove from your review wishes for all teams"> title="Remove from your review wishes for all teams">
@ -721,7 +721,7 @@
</i> </i>
Remove review wishes Remove review wishes
</a> </a>
<a class="btn btn-primary btn-sm review-wish-add-remove-doc {% if user.review_teams|length_is:"1" %}ajax {% endif %}{% if doc.has_review_wish %}d-none{% endif %}" <a class="btn btn-primary btn-sm review-wish-add-remove-doc {% if user.person.review_teams|length_is:"1" %}ajax {% endif %}{% if doc.has_review_wish %}d-none{% endif %}"
href="{% url "ietf.doc.views_review.review_wish_add" name=doc.name %}?next={{ request.get_full_path|urlencode }}" href="{% url "ietf.doc.views_review.review_wish_add" name=doc.name %}?next={{ request.get_full_path|urlencode }}"
title="Add to your review wishes"> title="Add to your review wishes">
<i class="bi bi-chat-left-heart"> <i class="bi bi-chat-left-heart">
@ -807,4 +807,4 @@
</script> </script>
<script src="{% static 'ietf/js/document_timeline.js' %}"> <script src="{% static 'ietf/js/document_timeline.js' %}">
</script> </script>
{% endblock %} {% endblock %}

View file

@ -129,14 +129,14 @@
</a> </a>
{% if user.is_authenticated %} {% if user.is_authenticated %}
<a class="btn btn-primary btn-sm track-untrack-doc {% if not doc.tracked_in_personal_community_list %}hide{% endif %}" <a class="btn btn-primary btn-sm track-untrack-doc {% if not doc.tracked_in_personal_community_list %}hide{% endif %}"
href="{% url "ietf.community.views.untrack_document" username=user.username name=doc.name %}" href="{% url "ietf.community.views.untrack_document" email_or_name=user.username name=doc.name %}"
title="Remove from your personal I-D list"> title="Remove from your personal I-D list">
<i class="bi bi-bookmark-check-fill"> <i class="bi bi-bookmark-check-fill">
</i> </i>
Untrack Untrack
</a> </a>
<a class="btn btn-primary btn-sm track-untrack-doc {% if doc.tracked_in_personal_community_list %}hide{% endif %}" <a class="btn btn-primary btn-sm track-untrack-doc {% if doc.tracked_in_personal_community_list %}hide{% endif %}"
href="{% url "ietf.community.views.track_document" username=user.username name=doc.name %}" href="{% url "ietf.community.views.track_document" email_or_name=user.username name=doc.name %}"
title="Add to your personal I-D list"> title="Add to your personal I-D list">
<i class="bi bi-bookmark"> <i class="bi bi-bookmark">
</i> </i>

View file

@ -9,13 +9,13 @@
<tr {% if color_ad_position %}{% with doc|ballotposition:user as pos %}{% if pos %}class="position-{{ pos.slug }}-row"{% endif %}{% endwith %}{% endif %}> <tr {% if color_ad_position %}{% with doc|ballotposition:user as pos %}{% if pos %}class="position-{{ pos.slug }}-row"{% endif %}{% endwith %}{% endif %}>
<td class="bg-transparent"> <td class="bg-transparent">
{% if user.is_authenticated %} {% if user.is_authenticated %}
<a href="{% url "ietf.community.views.untrack_document" username=request.user.username name=doc.name %}" <a href="{% url "ietf.community.views.untrack_document" email_or_name=request.user.username name=doc.name %}"
class="track-untrack-doc {% if not doc.tracked_in_personal_community_list %}d-none{% endif %}" class="track-untrack-doc {% if not doc.tracked_in_personal_community_list %}d-none{% endif %}"
aria-label="Remove from your personal I-D list" aria-label="Remove from your personal I-D list"
title="Remove from your personal I-D list"> title="Remove from your personal I-D list">
<i class="bi bi-bookmark-check-fill"></i> <i class="bi bi-bookmark-check-fill"></i>
</a> </a>
<a href="{% url "ietf.community.views.track_document" username=request.user.username name=doc.name %}" <a href="{% url "ietf.community.views.track_document" email_or_name=request.user.username name=doc.name %}"
class="track-untrack-doc {% if doc.tracked_in_personal_community_list %}d-none{% endif %}" class="track-untrack-doc {% if doc.tracked_in_personal_community_list %}d-none{% endif %}"
aria-label="Add to your personal I-D list" aria-label="Add to your personal I-D list"
title="Add to your personal I-D list"> title="Add to your personal I-D list">
@ -23,14 +23,14 @@
</a> </a>
<br> <br>
{% endif %} {% endif %}
{% if user.review_teams %} {% if user.person.review_teams %}
<a class="review-wish-add-remove-doc ajax {% if not doc.has_review_wish %}d-none{% endif %}" <a class="review-wish-add-remove-doc ajax {% if not doc.has_review_wish %}d-none{% endif %}"
href="{% url "ietf.doc.views_review.review_wishes_remove" name=doc.name %}?next={{ request.get_full_path|urlencode }}" href="{% url "ietf.doc.views_review.review_wishes_remove" name=doc.name %}?next={{ request.get_full_path|urlencode }}"
aria-label="Remove from your review wishes for all teams" aria-label="Remove from your review wishes for all teams"
title="Remove from your review wishes for all teams"> title="Remove from your review wishes for all teams">
<i class="bi bi-chat-left-heart-fill"></i> <i class="bi bi-chat-left-heart-fill"></i>
</a> </a>
<a class="review-wish-add-remove-doc {% if user.review_teams|length_is:"1" %}ajax {% endif %} {% if doc.has_review_wish %}d-none{% endif %}" <a class="review-wish-add-remove-doc {% if user.person.review_teams|length_is:"1" %}ajax {% endif %} {% if doc.has_review_wish %}d-none{% endif %}"
href="{% url "ietf.doc.views_review.review_wish_add" name=doc.name %}?next={{ request.get_full_path|urlencode }}" href="{% url "ietf.doc.views_review.review_wish_add" name=doc.name %}?next={{ request.get_full_path|urlencode }}"
aria-label="Add to your review wishes" aria-label="Add to your review wishes"
title="Add to your review wishes"> title="Add to your review wishes">
@ -153,4 +153,4 @@
{% endif %} {% endif %}
</td> </td>
{% endif %} {% endif %}
</tr> </tr>

View file

@ -32,7 +32,7 @@ class Command(BaseCommand):
person = rule.person person = rule.person
if not person and not group: if not person and not group:
try: try:
person = rule.community_list.user.person person = rule.community_list.person
except: except:
pass pass
name = ((group and group.acronym) or (person and person.email_address())) or '?' name = ((group and group.acronym) or (person and person.email_address())) or '?'

View file

@ -25,7 +25,6 @@ django-simple-history>=3.0.0
django-stubs>=4.2.7 # The django-stubs version used determines the the mypy version indicated below django-stubs>=4.2.7 # The django-stubs version used determines the the mypy version indicated below
django-tastypie>=0.14.5 # Version must be locked in sync with version of Django django-tastypie>=0.14.5 # Version must be locked in sync with version of Django
django-vite>=2.0.2,<3 django-vite>=2.0.2,<3
django-webtest>=1.9.10 # Only used in tests
django-widget-tweaks>=1.4.12 django-widget-tweaks>=1.4.12
djlint>=1.0.0 # To auto-indent templates via "djlint --profile django --reformat" djlint>=1.0.0 # To auto-indent templates via "djlint --profile django --reformat"
docutils>=0.18.1 # Used only by dbtemplates for RestructuredText docutils>=0.18.1 # Used only by dbtemplates for RestructuredText