commit
d31605e5a0
|
@ -34,7 +34,8 @@ $DTDIR/ietf/manage.py update_external_command_info
|
|||
rsync -avzq --delete /a/www/ietf-ftp/iana/yang-parameters/ /a/www/ietf-ftp/yang/ianamod/
|
||||
|
||||
# Get Yang models from Yangcatalog.
|
||||
rsync -avzq rsync://rsync.yangcatalog.org:10873/yangdeps /a/www/ietf-ftp/yang/catalogmod/
|
||||
#rsync -avzq rsync://rsync.yangcatalog.org:10873/yangdeps /a/www/ietf-ftp/yang/catalogmod/
|
||||
/a/www/ietf-datatracker/scripts/sync_to_yangcatalog
|
||||
|
||||
# Populate the yang repositories
|
||||
$DTDIR/ietf/manage.py populate_yang_model_dirs -v0
|
||||
|
|
|
@ -89,7 +89,7 @@ class PersonalInformationExportView(DetailView, JsonExportMixin):
|
|||
'sendqueue', 'nominee', 'topicfeedbacklastseen', 'alias', 'email', 'apikeys', 'personevent',
|
||||
'reviewersettings', 'reviewsecretarysettings', 'unavailableperiod', 'reviewwish',
|
||||
'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)
|
||||
|
|
|
@ -28,26 +28,6 @@ def already_ran():
|
|||
checks_run.append(name)
|
||||
return False
|
||||
|
||||
@checks.register('directories')
|
||||
def check_cdn_directory_exists(app_configs, **kwargs):
|
||||
"""This checks that the path from which the CDN will serve static files for
|
||||
this version of the datatracker actually exists. In development and test
|
||||
mode STATIC_ROOT will normally be just static/, but in production it will be
|
||||
set to a different part of the file system which is served via CDN, and the
|
||||
path will contain the datatracker release version.
|
||||
"""
|
||||
if already_ran():
|
||||
return []
|
||||
#
|
||||
errors = []
|
||||
if settings.SERVER_MODE == 'production' and not os.path.exists(settings.STATIC_ROOT):
|
||||
errors.append(checks.Error(
|
||||
"The static files directory has not been set up.",
|
||||
hint="Please run 'ietf/manage.py collectstatic'.",
|
||||
obj=None,
|
||||
id='datatracker.E001',
|
||||
))
|
||||
return errors
|
||||
|
||||
@checks.register('files')
|
||||
def check_group_email_aliases_exists(app_configs, **kwargs):
|
||||
|
|
|
@ -7,8 +7,8 @@ from django.contrib import admin
|
|||
from ietf.community.models import CommunityList, SearchRule, EmailSubscription
|
||||
|
||||
class CommunityListAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'user', 'group']
|
||||
raw_id_fields = ['user', 'group', 'added_docs']
|
||||
list_display = ['id', 'person', 'group']
|
||||
raw_id_fields = ['person', 'group', 'added_docs']
|
||||
admin.site.register(CommunityList, CommunityListAdmin)
|
||||
|
||||
class SearchRuleAdmin(admin.ModelAdmin):
|
||||
|
|
|
@ -114,14 +114,13 @@ class SearchRuleForm(forms.ModelForm):
|
|||
|
||||
|
||||
class SubscriptionForm(forms.ModelForm):
|
||||
def __init__(self, user, clist, *args, **kwargs):
|
||||
def __init__(self, person, clist, *args, **kwargs):
|
||||
self.clist = clist
|
||||
self.user = user
|
||||
|
||||
super(SubscriptionForm, self).__init__(*args, **kwargs)
|
||||
|
||||
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]])
|
||||
|
||||
if self.fields["email"].queryset:
|
||||
|
|
|
@ -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)]
|
54
ietf/community/migrations/0005_user_to_person.py
Normal file
54
ietf/community/migrations/0005_user_to_person.py
Normal 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),
|
||||
),
|
||||
]
|
|
@ -2,7 +2,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.db.models import signals
|
||||
from django.urls import reverse as urlreverse
|
||||
|
@ -13,13 +12,13 @@ from ietf.person.models import Person, Email
|
|||
from ietf.utils.models import ForeignKey
|
||||
|
||||
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)
|
||||
added_docs = models.ManyToManyField(Document)
|
||||
|
||||
def long_name(self):
|
||||
if self.user:
|
||||
return 'Personal I-D list of %s' % self.user.username
|
||||
if self.person:
|
||||
return 'Personal I-D list of %s' % self.person.plain_name()
|
||||
elif self.group:
|
||||
return 'I-D list for %s' % self.group.name
|
||||
else:
|
||||
|
@ -30,8 +29,8 @@ class CommunityList(models.Model):
|
|||
|
||||
def get_absolute_url(self):
|
||||
import ietf.community.views
|
||||
if self.user:
|
||||
return urlreverse(ietf.community.views.view_list, kwargs={ 'username': self.user.username })
|
||||
if self.person:
|
||||
return urlreverse(ietf.community.views.view_list, kwargs={ 'email_or_name': self.person.email() })
|
||||
elif self.group:
|
||||
return urlreverse("ietf.group.views.group_documents", kwargs={ 'acronym': self.group.acronym })
|
||||
return ""
|
||||
|
|
|
@ -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 -*-
|
||||
|
||||
|
||||
from pyquery import PyQuery
|
||||
|
||||
from django.urls import reverse as urlreverse
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from django_webtest import WebTest
|
||||
|
||||
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.doc.models import State
|
||||
from ietf.doc.utils import add_state_change_event
|
||||
from ietf.person.models import Person, Email
|
||||
from ietf.utils.test_utils import login_testing_unauthorized
|
||||
from ietf.person.models import Person, Email, Alias
|
||||
from ietf.utils.test_utils import TestCase, login_testing_unauthorized
|
||||
from ietf.utils.mail import outbox
|
||||
from ietf.doc.factories import WgDraftFactory
|
||||
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):
|
||||
plain = PersonFactory(user__username='plain')
|
||||
ad = Person.objects.get(user__username='ad')
|
||||
|
@ -38,7 +35,7 @@ class CommunityListTests(WebTest):
|
|||
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_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
|
||||
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):
|
||||
PersonFactory(user__username='plain')
|
||||
person = self.complex_person(user__username='plain')
|
||||
draft = WgDraftFactory()
|
||||
|
||||
url = urlreverse(ietf.community.views.view_list, kwargs={ "username": "plain" })
|
||||
|
||||
# without list
|
||||
for id in self.email_or_name_set(person):
|
||||
url = urlreverse(ietf.community.views.view_list, kwargs={ "email_or_name": id })
|
||||
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
|
||||
clist = CommunityList.objects.create(user=User.objects.get(username="plain"))
|
||||
clist = CommunityList.objects.create(person=person)
|
||||
if not draft in clist.added_docs.all():
|
||||
clist.added_docs.add(draft)
|
||||
SearchRule.objects.create(
|
||||
|
@ -109,42 +126,49 @@ class CommunityListTests(WebTest):
|
|||
state=State.objects.get(type="draft", slug="active"),
|
||||
text="test",
|
||||
)
|
||||
for id in self.email_or_name_set(person):
|
||||
url = urlreverse(ietf.community.views.view_list, kwargs={ "email_or_name": id })
|
||||
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)
|
||||
|
||||
def test_manage_personal_list(self):
|
||||
|
||||
PersonFactory(user__username='plain')
|
||||
person = self.complex_person(user__username='plain')
|
||||
ad = Person.objects.get(user__username='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)
|
||||
|
||||
page = self.app.get(url, user='plain')
|
||||
self.assertEqual(page.status_int, 200)
|
||||
for id in self.email_or_name_set(person):
|
||||
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}'")
|
||||
|
||||
# We can't call post() with follow=True because that 404's if
|
||||
# the url contains unicode, because the django test client
|
||||
# apparently re-encodes the already-encoded url.
|
||||
def follow(r):
|
||||
redirect_url = r.url or url
|
||||
return self.client.get(redirect_url, user='plain')
|
||||
|
||||
# add document
|
||||
self.assertIn('add_document', page.forms)
|
||||
form = page.forms['add_document']
|
||||
form['documents'].options=[(draft.pk, True, draft.name)]
|
||||
page = form.submit('action',value='add_documents')
|
||||
self.assertEqual(page.status_int, 302)
|
||||
clist = CommunityList.objects.get(user__username="plain")
|
||||
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))
|
||||
page = page.follow()
|
||||
|
||||
self.assertContains(page, draft.name)
|
||||
r = follow(r)
|
||||
self.assertContains(r, draft.name, status_code=200)
|
||||
|
||||
# remove document
|
||||
self.assertIn('remove_document_%s' % draft.pk, page.forms)
|
||||
form = page.forms['remove_document_%s' % draft.pk]
|
||||
page = form.submit('action',value='remove_document')
|
||||
self.assertEqual(page.status_int, 302)
|
||||
clist = CommunityList.objects.get(user__username="plain")
|
||||
self.assertContains(r, 'remove_document_%s' % draft.pk)
|
||||
r = self.client.post(url, {'action': 'remove_document', 'document': draft.pk})
|
||||
self.assertEqual(r.status_code, 302, msg=f"id='{id}', url='{url}'")
|
||||
clist = CommunityList.objects.get(person__user__username="plain")
|
||||
self.assertTrue(not clist.added_docs.filter(pk=draft.pk))
|
||||
page = page.follow()
|
||||
r = follow(r)
|
||||
self.assertNotContains(r, draft.name, status_code=200)
|
||||
|
||||
# add rule
|
||||
r = self.client.post(url, {
|
||||
|
@ -153,8 +177,8 @@ class CommunityListTests(WebTest):
|
|||
"author_rfc-person": Person.objects.filter(documentauthor__document=draft).first().pk,
|
||||
"author_rfc-state": State.objects.get(type="rfc", slug="published").pk,
|
||||
})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
clist = CommunityList.objects.get(user__username="plain")
|
||||
self.assertEqual(r.status_code, 302, msg=f"id='{id}', url='{url}'")
|
||||
clist = CommunityList.objects.get(person__user__username="plain")
|
||||
self.assertTrue(clist.searchrule_set.filter(rule_type="author_rfc"))
|
||||
|
||||
# add name_contains rule
|
||||
|
@ -164,13 +188,13 @@ class CommunityListTests(WebTest):
|
|||
"name_contains-text": "draft.*mars",
|
||||
"name_contains-state": State.objects.get(type="draft", slug="active").pk,
|
||||
})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
clist = CommunityList.objects.get(user__username="plain")
|
||||
self.assertEqual(r.status_code, 302, msg=f"id='{id}', url='{url}'")
|
||||
clist = CommunityList.objects.get(person__user__username="plain")
|
||||
self.assertTrue(clist.searchrule_set.filter(rule_type="name_contains"))
|
||||
|
||||
# rule shows up on GET
|
||||
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()
|
||||
q = PyQuery(r.content)
|
||||
self.assertEqual(len(q('#r%s' % rule.pk)), 1)
|
||||
|
@ -181,7 +205,7 @@ class CommunityListTests(WebTest):
|
|||
"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"))
|
||||
|
||||
def test_manage_group_list(self):
|
||||
|
@ -210,65 +234,72 @@ class CommunityListTests(WebTest):
|
|||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
def test_track_untrack_document(self):
|
||||
PersonFactory(user__username='plain')
|
||||
person = self.complex_person(user__username='plain')
|
||||
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)
|
||||
|
||||
for id in self.email_or_name_set(person):
|
||||
url = urlreverse(ietf.community.views.track_document, kwargs={ "email_or_name": id, "name": draft.name })
|
||||
|
||||
# track
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.status_code, 200, msg=f"id='{id}', url='{url}'")
|
||||
|
||||
r = self.client.post(url)
|
||||
self.assertEqual(r.status_code, 302)
|
||||
clist = CommunityList.objects.get(user__username="plain")
|
||||
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()), [draft])
|
||||
|
||||
# untrack
|
||||
url = urlreverse(ietf.community.views.untrack_document, kwargs={ "username": "plain", "name": draft.name })
|
||||
url = urlreverse(ietf.community.views.untrack_document, kwargs={ "email_or_name": id, "name": draft.name })
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.status_code, 200, msg=f"id='{id}', url='{url}'")
|
||||
|
||||
r = self.client.post(url)
|
||||
self.assertEqual(r.status_code, 302)
|
||||
clist = CommunityList.objects.get(user__username="plain")
|
||||
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):
|
||||
PersonFactory(user__username='plain')
|
||||
person = self.complex_person(user__username='plain')
|
||||
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)
|
||||
|
||||
for id in self.email_or_name_set(person):
|
||||
url = urlreverse(ietf.community.views.track_document, kwargs={ "email_or_name": id, "name": draft.name })
|
||||
|
||||
# track
|
||||
r = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.status_code, 200, msg=f"id='{id}', url='{url}'")
|
||||
self.assertEqual(r.json()["success"], True)
|
||||
clist = CommunityList.objects.get(user__username="plain")
|
||||
clist = CommunityList.objects.get(person__user__username="plain")
|
||||
self.assertEqual(list(clist.added_docs.all()), [draft])
|
||||
|
||||
# untrack
|
||||
url = urlreverse(ietf.community.views.untrack_document, kwargs={ "username": "plain", "name": draft.name })
|
||||
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)
|
||||
self.assertEqual(r.status_code, 200, msg=f"id='{id}', url='{url}'")
|
||||
self.assertEqual(r.json()["success"], True)
|
||||
clist = CommunityList.objects.get(user__username="plain")
|
||||
clist = CommunityList.objects.get(person__user__username="plain")
|
||||
self.assertEqual(list(clist.added_docs.all()), [])
|
||||
|
||||
def test_csv(self):
|
||||
PersonFactory(user__username='plain')
|
||||
person = self.complex_person(user__username='plain')
|
||||
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
|
||||
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
|
||||
clist = CommunityList.objects.create(user=User.objects.get(username="plain"))
|
||||
clist = CommunityList.objects.create(person=person)
|
||||
if not draft in clist.added_docs.all():
|
||||
clist.added_docs.add(draft)
|
||||
SearchRule.objects.create(
|
||||
|
@ -278,7 +309,7 @@ class CommunityListTests(WebTest):
|
|||
text="test",
|
||||
)
|
||||
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
|
||||
self.assertContains(r, draft.name)
|
||||
|
||||
|
@ -294,17 +325,18 @@ class CommunityListTests(WebTest):
|
|||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
def test_feed(self):
|
||||
PersonFactory(user__username='plain')
|
||||
person = self.complex_person(user__username='plain')
|
||||
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
|
||||
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
|
||||
clist = CommunityList.objects.create(user=User.objects.get(username="plain"))
|
||||
clist = CommunityList.objects.create(person=person)
|
||||
if not draft in clist.added_docs.all():
|
||||
clist.added_docs.add(draft)
|
||||
SearchRule.objects.create(
|
||||
|
@ -314,12 +346,12 @@ class CommunityListTests(WebTest):
|
|||
text="test",
|
||||
)
|
||||
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)
|
||||
|
||||
# only significant
|
||||
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>')
|
||||
|
||||
def test_feed_for_group(self):
|
||||
|
@ -334,19 +366,21 @@ class CommunityListTests(WebTest):
|
|||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
def test_subscription(self):
|
||||
PersonFactory(user__username='plain')
|
||||
person = self.complex_person(user__username='plain')
|
||||
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)
|
||||
|
||||
for id in self.email_or_name_set(person):
|
||||
url = urlreverse(ietf.community.views.subscription, kwargs={ "email_or_name": id })
|
||||
|
||||
# subscription without list
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
self.assertEqual(r.status_code, 404, msg=f"id='{id}', url='{url}'")
|
||||
|
||||
# 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():
|
||||
clist.added_docs.add(draft)
|
||||
SearchRule.objects.create(
|
||||
|
@ -355,11 +389,14 @@ class CommunityListTests(WebTest):
|
|||
state=State.objects.get(type="draft", slug="active"),
|
||||
text="test",
|
||||
)
|
||||
|
||||
for email in Email.objects.filter(person=person):
|
||||
url = urlreverse(ietf.community.views.subscription, kwargs={ "email_or_name": email })
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# subscribe
|
||||
email = Email.objects.filter(person__user__username="plain").first()
|
||||
r = self.client.post(url, { "email": email.pk, "notify_on": "significant", "action": "subscribe" })
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
|
@ -387,10 +424,10 @@ class CommunityListTests(WebTest):
|
|||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
def test_notification(self):
|
||||
PersonFactory(user__username='plain')
|
||||
person = PersonFactory(user__username='plain')
|
||||
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():
|
||||
clist.added_docs.add(draft)
|
||||
|
||||
|
|
|
@ -4,11 +4,11 @@ from ietf.community import views
|
|||
from ietf.utils.urls import url
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^personal/(?P<username>[^/]+)/$', views.view_list),
|
||||
url(r'^personal/(?P<username>[^/]+)/manage/$', views.manage_list),
|
||||
url(r'^personal/(?P<username>[^/]+)/trackdocument/(?P<name>[^/]+)/$', views.track_document),
|
||||
url(r'^personal/(?P<username>[^/]+)/untrackdocument/(?P<name>[^/]+)/$', views.untrack_document),
|
||||
url(r'^personal/(?P<username>[^/]+)/csv/$', views.export_to_csv),
|
||||
url(r'^personal/(?P<username>[^/]+)/feed/$', views.feed),
|
||||
url(r'^personal/(?P<username>[^/]+)/subscription/$', views.subscription),
|
||||
url(r'^personal/(?P<email_or_name>[^/]+)/$', views.view_list),
|
||||
url(r'^personal/(?P<email_or_name>[^/]+)/manage/$', views.manage_list),
|
||||
url(r'^personal/(?P<email_or_name>[^/]+)/trackdocument/(?P<name>[^/]+)/$', views.track_document),
|
||||
url(r'^personal/(?P<email_or_name>[^/]+)/untrackdocument/(?P<name>[^/]+)/$', views.untrack_document),
|
||||
url(r'^personal/(?P<email_or_name>[^/]+)/csv/$', views.export_to_csv),
|
||||
url(r'^personal/(?P<email_or_name>[^/]+)/feed/$', views.feed),
|
||||
url(r'^personal/(?P<email_or_name>[^/]+)/subscription/$', views.subscription),
|
||||
]
|
||||
|
|
|
@ -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 -*-
|
||||
|
||||
|
||||
|
@ -11,11 +11,9 @@ import debug # pyflakes:ignore
|
|||
|
||||
from ietf.community.models import CommunityList, EmailSubscription, SearchRule
|
||||
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.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
|
||||
|
||||
|
@ -29,24 +27,12 @@ def states_of_significant_change():
|
|||
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):
|
||||
if not user or not user.is_authenticated:
|
||||
return False
|
||||
|
||||
if clist.user:
|
||||
return user == clist.user
|
||||
if clist.person:
|
||||
return user == clist.person.user
|
||||
elif clist.group:
|
||||
if has_role(user, 'Secretariat'):
|
||||
return True
|
||||
|
|
|
@ -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 -*-
|
||||
|
||||
|
||||
|
@ -15,18 +15,46 @@ from django.utils.html import strip_tags
|
|||
|
||||
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.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 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.utils_search import prepare_document_table
|
||||
from ietf.person.utils import lookup_persons
|
||||
from ietf.utils.http import is_ajax
|
||||
from ietf.utils.response import permission_denied
|
||||
|
||||
def view_list(request, username=None):
|
||||
clist = lookup_community_list(username)
|
||||
class MultiplePersonError(Exception):
|
||||
"""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, meta = prepare_document_table(request, docs, request.GET)
|
||||
|
@ -42,10 +70,13 @@ def view_list(request, username=None):
|
|||
})
|
||||
|
||||
@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
|
||||
# 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):
|
||||
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
|
||||
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)
|
||||
|
||||
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):
|
||||
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
|
||||
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)
|
||||
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):
|
||||
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):
|
||||
clist = lookup_community_list(username, acronym)
|
||||
def export_to_csv(request, email_or_name=None, acronym=None):
|
||||
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')
|
||||
|
||||
|
@ -213,8 +253,11 @@ def export_to_csv(request, username=None, acronym=None, group_type=None):
|
|||
|
||||
return response
|
||||
|
||||
def feed(request, username=None, acronym=None, group_type=None):
|
||||
clist = lookup_community_list(username, acronym)
|
||||
def feed(request, email_or_name=None, acronym=None):
|
||||
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'
|
||||
|
||||
|
@ -249,17 +292,22 @@ def feed(request, username=None, acronym=None, group_type=None):
|
|||
|
||||
|
||||
@login_required
|
||||
def subscription(request, username=None, acronym=None, group_type=None):
|
||||
clist = lookup_community_list(username, acronym)
|
||||
def subscription(request, email_or_name=None, acronym=None):
|
||||
try:
|
||||
clist = lookup_community_list(request, email_or_name, acronym)
|
||||
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':
|
||||
action = request.POST.get("action")
|
||||
if action == "subscribe":
|
||||
form = SubscriptionForm(request.user, clist, request.POST)
|
||||
form = SubscriptionForm(person, clist, request.POST)
|
||||
if form.is_valid():
|
||||
subscription = form.save(commit=False)
|
||||
subscription.community_list = clist
|
||||
|
@ -272,7 +320,7 @@ def subscription(request, username=None, acronym=None, group_type=None):
|
|||
|
||||
return HttpResponseRedirect("")
|
||||
else:
|
||||
form = SubscriptionForm(request.user, clist)
|
||||
form = SubscriptionForm(person, clist)
|
||||
|
||||
return render(request, 'community/subscription.html', {
|
||||
'clist': clist,
|
||||
|
|
69
ietf/doc/management/commands/reset_rfc_authors.py
Normal file
69
ietf/doc/management/commands/reset_rfc_authors.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
# Copyright The IETF Trust 2024, All Rights Reserved
|
||||
|
||||
# Reset an RFC's authors to those of the draft it came from
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from ietf.doc.models import Document, DocEvent
|
||||
from ietf.person.models import Person
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("rfcnum", type=int, help="RFC number to modify")
|
||||
parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="reset even if RFC already has authors",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
try:
|
||||
rfc = Document.objects.get(type="rfc", rfc_number=options["rfcnum"])
|
||||
except Document.DoesNotExist:
|
||||
raise CommandError(
|
||||
f"rfc{options['rfcnum']} does not exist in the Datatracker."
|
||||
)
|
||||
|
||||
draft = rfc.came_from_draft()
|
||||
if draft is None:
|
||||
raise CommandError(f"{rfc.name} did not come from a draft. Can't reset.")
|
||||
|
||||
orig_authors = rfc.documentauthor_set.all()
|
||||
if orig_authors.exists():
|
||||
# Potentially dangerous, so refuse unless "--force" is specified
|
||||
if not options["force"]:
|
||||
raise CommandError(
|
||||
f"{rfc.name} already has authors. Not resetting. Use '--force' to reset anyway."
|
||||
)
|
||||
removed_auth_names = list(orig_authors.values_list("person__name", flat=True))
|
||||
rfc.documentauthor_set.all().delete()
|
||||
DocEvent.objects.create(
|
||||
doc=rfc,
|
||||
by=Person.objects.get(name="(System)"),
|
||||
type="edited_authors",
|
||||
desc=f"Removed all authors: {', '.join(removed_auth_names)}",
|
||||
)
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Removed author(s): {', '.join(removed_auth_names)}"
|
||||
)
|
||||
)
|
||||
|
||||
for author in draft.documentauthor_set.all():
|
||||
# Copy the author but point at the new doc.
|
||||
# See https://docs.djangoproject.com/en/4.2/topics/db/queries/#copying-model-instances
|
||||
author.pk = None
|
||||
author.id = None
|
||||
author._state.adding = True
|
||||
author.document = rfc
|
||||
author.save()
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"Added author {author.person.name} <{author.email}>")
|
||||
)
|
||||
auth_names = draft.documentauthor_set.values_list("person__name", flat=True)
|
||||
DocEvent.objects.create(
|
||||
doc=rfc,
|
||||
by=Person.objects.get(name="(System)"),
|
||||
type="edited_authors",
|
||||
desc=f"Set authors from rev {draft.rev} of {draft.name}: {', '.join(auth_names)}",
|
||||
)
|
72
ietf/doc/management/commands/tests.py
Normal file
72
ietf/doc/management/commands/tests.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
# Copyright The IETF Trust 2024, All Rights Reserved
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from io import StringIO
|
||||
|
||||
from django.core.management import call_command, CommandError
|
||||
|
||||
from ietf.doc.factories import DocumentAuthorFactory, WgDraftFactory, WgRfcFactory
|
||||
from ietf.doc.models import Document, DocumentAuthor
|
||||
from ietf.utils.test_utils import TestCase
|
||||
|
||||
|
||||
class CommandTests(TestCase):
|
||||
@staticmethod
|
||||
def _call_command(command_name, *args, **options):
|
||||
"""Call command, capturing (and suppressing) output"""
|
||||
out = StringIO()
|
||||
err = StringIO()
|
||||
options["stdout"] = out
|
||||
options["stderr"] = err
|
||||
call_command(command_name, *args, **options)
|
||||
return out.getvalue(), err.getvalue()
|
||||
|
||||
def test_reset_rfc_authors(self):
|
||||
command_name = "reset_rfc_authors"
|
||||
|
||||
draft = WgDraftFactory()
|
||||
DocumentAuthorFactory.create_batch(3, document=draft)
|
||||
rfc = WgRfcFactory() # rfc does not yet have a draft
|
||||
DocumentAuthorFactory.create_batch(3, document=rfc)
|
||||
bad_rfc_num = (
|
||||
1
|
||||
+ Document.objects.filter(rfc_number__isnull=False)
|
||||
.order_by("-rfc_number")
|
||||
.first()
|
||||
.rfc_number
|
||||
)
|
||||
docauthor_fields = [
|
||||
field.name
|
||||
for field in DocumentAuthor._meta.get_fields()
|
||||
if field.name not in ["document", "id"]
|
||||
]
|
||||
|
||||
with self.assertRaises(CommandError, msg="Cannot reset a bad RFC number"):
|
||||
self._call_command(command_name, bad_rfc_num)
|
||||
|
||||
with self.assertRaises(CommandError, msg="Cannot reset an RFC with no draft"):
|
||||
self._call_command(command_name, rfc.rfc_number)
|
||||
|
||||
with self.assertRaises(CommandError, msg="Cannot force-reset an RFC with no draft"):
|
||||
self._call_command(command_name, rfc.rfc_number, "--force")
|
||||
|
||||
# Link the draft to the rfc
|
||||
rfc.targets_related.create(relationship_id="became_rfc", source=draft)
|
||||
|
||||
with self.assertRaises(CommandError, msg="Cannot reset an RFC with authors"):
|
||||
self._call_command(command_name, rfc.rfc_number)
|
||||
|
||||
# Calling with force should work
|
||||
self._call_command(command_name, rfc.rfc_number, "--force")
|
||||
self.assertCountEqual(
|
||||
draft.documentauthor_set.values(*docauthor_fields),
|
||||
rfc.documentauthor_set.values(*docauthor_fields),
|
||||
)
|
||||
|
||||
# Calling on an RFC with no authors should also work
|
||||
rfc.documentauthor_set.all().delete()
|
||||
self._call_command(command_name, rfc.rfc_number)
|
||||
self.assertCountEqual(
|
||||
draft.documentauthor_set.values(*docauthor_fields),
|
||||
rfc.documentauthor_set.values(*docauthor_fields),
|
||||
)
|
|
@ -1081,29 +1081,26 @@ def build_file_urls(doc: Union[Document, DocHistory]):
|
|||
|
||||
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
|
||||
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()
|
||||
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')
|
||||
# used in templates
|
||||
person.review_teams = Group.objects.filter(
|
||||
reviewteamsettings__isnull=False, role__person=person, role__name='reviewer')
|
||||
|
||||
doc_pks = [d.pk for d in docs]
|
||||
clist = CommunityList.objects.filter(user=user).first()
|
||||
clist = CommunityList.objects.filter(person=person).first()
|
||||
if clist:
|
||||
tracked.update(
|
||||
docs_tracked_by_community_list(clist).filter(pk__in=doc_pks).values_list("pk", flat=True))
|
||||
|
||||
try:
|
||||
wishes = ReviewWish.objects.filter(person=Person.objects.get(user=user))
|
||||
wishes = ReviewWish.objects.filter(person=person)
|
||||
wishes = wishes.filter(doc__pk__in=doc_pks).values_list("doc__pk", flat=True)
|
||||
review_wished.update(wishes)
|
||||
except Person.DoesNotExist:
|
||||
pass
|
||||
|
||||
for d in docs:
|
||||
d.tracked_in_personal_community_list = d.pk in tracked
|
||||
|
|
|
@ -11,7 +11,7 @@ from django.conf import settings
|
|||
|
||||
from ietf.doc.models import Document, RelatedDocument, DocEvent, TelechatDocEvent, BallotDocEvent, DocTypeName
|
||||
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.review.utils import review_assignments_to_list_for_docs
|
||||
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]
|
||||
|
||||
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)
|
||||
|
||||
meta = {}
|
||||
|
|
|
@ -62,7 +62,7 @@ from ietf.doc.utils import (augment_events_with_revision,
|
|||
needed_ballot_positions, nice_consensus, update_telechat, has_same_ballot,
|
||||
get_initial_notify, make_notify_changed_event, make_rev_history, default_consensus,
|
||||
add_events_message_info, get_unicode_document_content,
|
||||
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,
|
||||
bibxml_for_draft)
|
||||
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()
|
||||
|
||||
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")
|
||||
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')):
|
||||
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
|
||||
started_iesg_process = doc.latest_event(type="started_iesg_process")
|
||||
|
|
|
@ -366,10 +366,6 @@ class Meeting(models.Model):
|
|||
pass
|
||||
return None
|
||||
|
||||
def set_official_schedule(self, schedule):
|
||||
if self.schedule != schedule:
|
||||
self.schedule = schedule
|
||||
self.save()
|
||||
|
||||
def updated(self):
|
||||
# should be Meeting.modified, but we don't have that
|
||||
|
@ -457,22 +453,6 @@ class Room(models.Model):
|
|||
def __str__(self):
|
||||
return u"%s size: %s" % (self.name, self.capacity)
|
||||
|
||||
def delete_timeslots(self):
|
||||
for ts in self.timeslot_set.all():
|
||||
ts.sessionassignments.all().delete()
|
||||
ts.delete()
|
||||
|
||||
def create_timeslots(self):
|
||||
days, time_slices, slots = self.meeting.build_timeslices()
|
||||
for day in days:
|
||||
for ts in slots[day]:
|
||||
TimeSlot.objects.create(type_id=ts.type_id,
|
||||
meeting=self.meeting,
|
||||
name=ts.name,
|
||||
time=ts.time,
|
||||
location=self,
|
||||
duration=ts.duration)
|
||||
#self.meeting.create_all_timeslots()
|
||||
|
||||
def dom_id(self):
|
||||
return "room%u" % (self.pk)
|
||||
|
@ -496,14 +476,6 @@ class Room(models.Model):
|
|||
return max(self.x1, self.x2) if (self.x1 and self.x2) else 0
|
||||
def bottom(self):
|
||||
return max(self.y1, self.y2) if (self.y1 and self.y2) else 0
|
||||
def functional_display_name(self):
|
||||
if not self.functional_name:
|
||||
return ""
|
||||
if 'breakout' in self.functional_name.lower():
|
||||
return ""
|
||||
if self.functional_name[0].isdigit():
|
||||
return ""
|
||||
return self.functional_name
|
||||
# audio stream support
|
||||
def audio_stream_url(self):
|
||||
urlresources = [ur for ur in self.urlresource_set.all() if ur.name_id == 'audiostream']
|
||||
|
@ -775,9 +747,6 @@ class Schedule(models.Model):
|
|||
else:
|
||||
return "unofficial"
|
||||
|
||||
def delete_assignments(self):
|
||||
self.assignments.all().delete()
|
||||
|
||||
@property
|
||||
def qs_assignments_with_sessions(self):
|
||||
return self.assignments.filter(session__isnull=False)
|
||||
|
@ -790,10 +759,6 @@ class Schedule(models.Model):
|
|||
"""Get QuerySet containing sessions assigned to timeslots by this schedule"""
|
||||
return Session.objects.filter(timeslotassignments__schedule=self)
|
||||
|
||||
def delete_schedule(self):
|
||||
self.assignments.all().delete()
|
||||
self.delete()
|
||||
|
||||
# to be renamed SchedTimeSessAssignments (stsa)
|
||||
class SchedTimeSessAssignment(models.Model):
|
||||
"""
|
||||
|
@ -1143,30 +1108,6 @@ class Session(models.Model):
|
|||
self._order_in_meeting = session_list.index(self) + 1 if self in session_list else 0
|
||||
return self._order_in_meeting
|
||||
|
||||
def all_meeting_sessions_cancelled(self):
|
||||
return set(s.current_status for s in self.all_meeting_sessions_for_group()) == {'canceled'}
|
||||
|
||||
def all_meeting_recordings(self):
|
||||
recordings = [] # These are not sets because we need to preserve relative ordering or redo the ordering work later
|
||||
sessions = self.all_meeting_sessions_for_group()
|
||||
for session in sessions:
|
||||
recordings.extend([r for r in session.recordings() if r not in recordings])
|
||||
return recordings
|
||||
|
||||
def all_meeting_bluesheets(self):
|
||||
bluesheets = []
|
||||
sessions = self.all_meeting_sessions_for_group()
|
||||
for session in sessions:
|
||||
bluesheets.extend([b for b in session.bluesheets() if b not in bluesheets])
|
||||
return bluesheets
|
||||
|
||||
def all_meeting_drafts(self):
|
||||
drafts = []
|
||||
sessions = self.all_meeting_sessions_for_group()
|
||||
for session in sessions:
|
||||
drafts.extend([d for d in session.drafts() if d not in drafts])
|
||||
return drafts
|
||||
|
||||
def all_meeting_agendas(self):
|
||||
agendas = []
|
||||
sessions = self.all_meeting_sessions_for_group()
|
||||
|
@ -1283,19 +1224,6 @@ class Session(models.Model):
|
|||
else:
|
||||
return "The agenda has not been uploaded yet."
|
||||
|
||||
def agenda_file(self):
|
||||
if not hasattr(self, '_agenda_file'):
|
||||
self._agenda_file = ""
|
||||
|
||||
agenda = self.agenda()
|
||||
if not agenda:
|
||||
return ""
|
||||
|
||||
# FIXME: uploaded_filename should be replaced with a function that computes filenames when they are of a fixed schema and not uploaded names
|
||||
self._agenda_file = "%s/agenda/%s" % (self.meeting.number, agenda.uploaded_filename)
|
||||
|
||||
return self._agenda_file
|
||||
|
||||
def chat_room_name(self):
|
||||
if self.chat_room:
|
||||
return self.chat_room
|
||||
|
|
27
ietf/message/factories.py
Normal file
27
ietf/message/factories.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Copyright The IETF Trust 2024, All Rights Reserved
|
||||
import factory
|
||||
|
||||
from ietf.person.models import Person
|
||||
from .models import Message, SendQueue
|
||||
|
||||
|
||||
class MessageFactory(factory.django.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = Message
|
||||
|
||||
by = factory.LazyFunction(lambda: Person.objects.get(name="(System)"))
|
||||
subject = factory.Faker("sentence")
|
||||
to = factory.Faker("email")
|
||||
frm = factory.Faker("email")
|
||||
cc = factory.Faker("email")
|
||||
bcc = factory.Faker("email")
|
||||
body = factory.Faker("paragraph")
|
||||
content_type = "text/plain"
|
||||
|
||||
|
||||
class SendQueueFactory(factory.django.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = SendQueue
|
||||
|
||||
by = factory.LazyFunction(lambda: Person.objects.get(name="(System)"))
|
||||
message = factory.SubFactory(MessageFactory)
|
27
ietf/message/tasks.py
Normal file
27
ietf/message/tasks.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Copyright The IETF Trust 2024 All Rights Reserved
|
||||
#
|
||||
# Celery task definitions
|
||||
#
|
||||
from celery import shared_task
|
||||
from smtplib import SMTPException
|
||||
|
||||
from ietf.message.utils import send_scheduled_message_from_send_queue
|
||||
from ietf.message.models import SendQueue
|
||||
from ietf.utils import log
|
||||
from ietf.utils.mail import log_smtp_exception, send_error_email
|
||||
|
||||
|
||||
@shared_task
|
||||
def send_scheduled_mail_task():
|
||||
"""Send scheduled email
|
||||
|
||||
This is equivalent to `ietf/bin/send-scheduled-mail all`, which was the only form used in the cron job.
|
||||
"""
|
||||
needs_sending = SendQueue.objects.filter(sent_at=None).select_related("message")
|
||||
for s in needs_sending:
|
||||
try:
|
||||
send_scheduled_message_from_send_queue(s)
|
||||
log.log('Sent scheduled message %s "%s"' % (s.id, s.message.subject))
|
||||
except SMTPException as e:
|
||||
log_smtp_exception(e)
|
||||
send_error_email(e)
|
|
@ -1,8 +1,9 @@
|
|||
# Copyright The IETF Trust 2013-2020, All Rights Reserved
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import datetime
|
||||
import mock
|
||||
|
||||
from smtplib import SMTPException
|
||||
|
||||
from django.urls import reverse as urlreverse
|
||||
from django.utils import timezone
|
||||
|
@ -10,7 +11,9 @@ from django.utils import timezone
|
|||
import debug # pyflakes:ignore
|
||||
|
||||
from ietf.group.factories import GroupFactory
|
||||
from ietf.message.factories import SendQueueFactory
|
||||
from ietf.message.models import Message, SendQueue
|
||||
from ietf.message.tasks import send_scheduled_mail_task
|
||||
from ietf.message.utils import send_scheduled_message_from_send_queue
|
||||
from ietf.person.models import Person
|
||||
from ietf.utils.mail import outbox, send_mail_text, send_mail_message, get_payload_text
|
||||
|
@ -128,3 +131,22 @@ class SendScheduledAnnouncementsTests(TestCase):
|
|||
self.assertTrue("This is a test" in outbox[-1]["Subject"])
|
||||
self.assertTrue("--NextPart" in outbox[-1].as_string())
|
||||
self.assertTrue(SendQueue.objects.get(id=q.id).sent_at)
|
||||
|
||||
|
||||
class TaskTests(TestCase):
|
||||
@mock.patch("ietf.message.tasks.log_smtp_exception")
|
||||
@mock.patch("ietf.message.tasks.send_scheduled_message_from_send_queue")
|
||||
def test_send_scheduled_mail_task(self, mock_send_message, mock_log_smtp_exception):
|
||||
not_yet_sent = SendQueueFactory()
|
||||
SendQueueFactory(sent_at=timezone.now()) # already sent
|
||||
send_scheduled_mail_task()
|
||||
self.assertEqual(mock_send_message.call_count, 1)
|
||||
self.assertEqual(mock_send_message.call_args[0], (not_yet_sent,))
|
||||
self.assertFalse(mock_log_smtp_exception.called)
|
||||
|
||||
mock_send_message.reset_mock()
|
||||
mock_send_message.side_effect = SMTPException
|
||||
send_scheduled_mail_task()
|
||||
self.assertEqual(mock_send_message.call_count, 1)
|
||||
self.assertEqual(mock_send_message.call_args[0], (not_yet_sent,))
|
||||
self.assertTrue(mock_log_smtp_exception.called)
|
||||
|
|
|
@ -21,9 +21,9 @@ class NomComAdmin(admin.ModelAdmin):
|
|||
admin.site.register(NomCom, NomComAdmin)
|
||||
|
||||
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']
|
||||
raw_id_fields = ['nominee', 'comments', 'user']
|
||||
raw_id_fields = ['nominee', 'comments', 'person']
|
||||
admin.site.register(Nomination, NominationAdmin)
|
||||
|
||||
class NomineeAdmin(admin.ModelAdmin):
|
||||
|
@ -51,9 +51,9 @@ class FeedbackAdmin(admin.ModelAdmin):
|
|||
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
|
||||
|
||||
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', ]
|
||||
raw_id_fields = ['positions', 'topics', 'user']
|
||||
raw_id_fields = ['positions', 'topics', 'person']
|
||||
admin.site.register(Feedback, FeedbackAdmin)
|
||||
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ from faker import Faker
|
|||
|
||||
from ietf.nomcom.models import NomCom, Position, Feedback, Nominee, NomineePosition, Nomination, Topic
|
||||
from ietf.group.factories import GroupFactory
|
||||
from ietf.person.factories import PersonFactory, UserFactory
|
||||
from ietf.person.factories import PersonFactory
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
|
@ -199,7 +199,7 @@ class NominationFactory(factory.django.DjangoModelFactory):
|
|||
candidate_email = factory.LazyAttribute(lambda obj: obj.nominee.person.email())
|
||||
candidate_phone = factory.Faker('phone_number')
|
||||
comments = factory.SubFactory(FeedbackFactory)
|
||||
nominator_email = factory.LazyAttribute(lambda obj: obj.user.email)
|
||||
user = factory.SubFactory(UserFactory)
|
||||
nominator_email = factory.LazyAttribute(lambda obj: obj.person.user.email)
|
||||
person = factory.SubFactory(PersonFactory)
|
||||
share_nominator = False
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ from ietf.name.models import FeedbackTypeName, NomineePositionStateName
|
|||
from ietf.nomcom.models import ( NomCom, Nomination, Nominee, NomineePosition,
|
||||
Position, Feedback, ReminderDates, Topic, Volunteer )
|
||||
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,
|
||||
create_feedback_email)
|
||||
from ietf.person.models import Email
|
||||
|
@ -256,7 +256,7 @@ class NominateForm(forms.ModelForm):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.nomcom = kwargs.pop('nomcom', None)
|
||||
self.user = kwargs.pop('user', None)
|
||||
self.person = kwargs.pop('person', None)
|
||||
self.public = kwargs.pop('public', None)
|
||||
|
||||
super(NominateForm, self).__init__(*args, **kwargs)
|
||||
|
@ -273,7 +273,7 @@ class NominateForm(forms.ModelForm):
|
|||
|
||||
if not self.public:
|
||||
self.fields.pop('confirmation')
|
||||
author = get_user_email(self.user)
|
||||
author = get_person_email(self.person)
|
||||
if author:
|
||||
self.fields['nominator_email'].initial = author.address
|
||||
help_text = """(Nomcom Chair/Member: please fill this in. Use your own email address if the person making the
|
||||
|
@ -303,7 +303,7 @@ class NominateForm(forms.ModelForm):
|
|||
|
||||
author = None
|
||||
if self.public:
|
||||
author = get_user_email(self.user)
|
||||
author = get_person_email(self.person)
|
||||
else:
|
||||
if nominator_email:
|
||||
emails = Email.objects.filter(address=nominator_email)
|
||||
|
@ -314,7 +314,7 @@ class NominateForm(forms.ModelForm):
|
|||
feedback = Feedback.objects.create(nomcom=self.nomcom,
|
||||
comments=self.nomcom.encrypt(qualifications),
|
||||
type=FeedbackTypeName.objects.get(slug='nomina'),
|
||||
user=self.user)
|
||||
person=self.person)
|
||||
feedback.positions.add(position)
|
||||
feedback.nominees.add(nominee)
|
||||
|
||||
|
@ -326,7 +326,7 @@ class NominateForm(forms.ModelForm):
|
|||
nomination.nominee = nominee
|
||||
nomination.comments = feedback
|
||||
nomination.share_nominator = share_nominator
|
||||
nomination.user = self.user
|
||||
nomination.person = self.person
|
||||
|
||||
if commit:
|
||||
nomination.save()
|
||||
|
@ -361,7 +361,7 @@ class NominateNewPersonForm(forms.ModelForm):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.nomcom = kwargs.pop('nomcom', None)
|
||||
self.user = kwargs.pop('user', None)
|
||||
self.person = kwargs.pop('person', None)
|
||||
self.public = kwargs.pop('public', None)
|
||||
|
||||
super(NominateNewPersonForm, self).__init__(*args, **kwargs)
|
||||
|
@ -375,7 +375,7 @@ class NominateNewPersonForm(forms.ModelForm):
|
|||
|
||||
if not self.public:
|
||||
self.fields.pop('confirmation')
|
||||
author = get_user_email(self.user)
|
||||
author = get_person_email(self.person)
|
||||
if author:
|
||||
self.fields['nominator_email'].initial = author.address
|
||||
help_text = """(Nomcom Chair/Member: please fill this in. Use your own email address if the person making the
|
||||
|
@ -416,7 +416,7 @@ class NominateNewPersonForm(forms.ModelForm):
|
|||
|
||||
author = None
|
||||
if self.public:
|
||||
author = get_user_email(self.user)
|
||||
author = get_person_email(self.person)
|
||||
else:
|
||||
if nominator_email:
|
||||
emails = Email.objects.filter(address=nominator_email)
|
||||
|
@ -429,7 +429,7 @@ class NominateNewPersonForm(forms.ModelForm):
|
|||
feedback = Feedback.objects.create(nomcom=self.nomcom,
|
||||
comments=self.nomcom.encrypt(qualifications),
|
||||
type=FeedbackTypeName.objects.get(slug='nomina'),
|
||||
user=self.user)
|
||||
person=self.person)
|
||||
feedback.positions.add(position)
|
||||
feedback.nominees.add(nominee)
|
||||
|
||||
|
@ -441,7 +441,7 @@ class NominateNewPersonForm(forms.ModelForm):
|
|||
nomination.nominee = nominee
|
||||
nomination.comments = feedback
|
||||
nomination.share_nominator = share_nominator
|
||||
nomination.user = self.user
|
||||
nomination.person = self.person
|
||||
|
||||
if commit:
|
||||
nomination.save()
|
||||
|
@ -476,7 +476,7 @@ class FeedbackForm(forms.ModelForm):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
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.position = kwargs.pop('position', None)
|
||||
self.nominee = kwargs.pop('nominee', None)
|
||||
|
@ -484,7 +484,7 @@ class FeedbackForm(forms.ModelForm):
|
|||
|
||||
super(FeedbackForm, self).__init__(*args, **kwargs)
|
||||
|
||||
author = get_user_email(self.user)
|
||||
author = get_person_email(self.person)
|
||||
|
||||
if self.public:
|
||||
self.fields.pop('nominator_email')
|
||||
|
@ -514,7 +514,7 @@ class FeedbackForm(forms.ModelForm):
|
|||
|
||||
author = None
|
||||
if self.public:
|
||||
author = get_user_email(self.user)
|
||||
author = get_person_email(self.person)
|
||||
else:
|
||||
nominator_email = self.cleaned_data['nominator_email']
|
||||
if nominator_email:
|
||||
|
@ -525,7 +525,7 @@ class FeedbackForm(forms.ModelForm):
|
|||
feedback.author = author.address
|
||||
|
||||
feedback.nomcom = self.nomcom
|
||||
feedback.user = self.user
|
||||
feedback.person = self.person
|
||||
feedback.type = FeedbackTypeName.objects.get(slug='comment')
|
||||
feedback.comments = self.nomcom.encrypt(comment_text)
|
||||
feedback.save()
|
||||
|
@ -578,7 +578,7 @@ class QuestionnaireForm(forms.ModelForm):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.nomcom = kwargs.pop('nomcom', None)
|
||||
self.user = kwargs.pop('user', None)
|
||||
self.person = kwargs.pop('person', None)
|
||||
|
||||
super(QuestionnaireForm, self).__init__(*args, **kwargs)
|
||||
self.fields['nominee'] = PositionNomineeField(nomcom=self.nomcom, required=True)
|
||||
|
@ -588,13 +588,13 @@ class QuestionnaireForm(forms.ModelForm):
|
|||
comment_text = self.cleaned_data['comment_text']
|
||||
(position, nominee) = self.cleaned_data['nominee']
|
||||
|
||||
author = get_user_email(self.user)
|
||||
author = get_person_email(self.person)
|
||||
|
||||
if author:
|
||||
feedback.author = author
|
||||
|
||||
feedback.nomcom = self.nomcom
|
||||
feedback.user = self.user
|
||||
feedback.person = self.person
|
||||
feedback.type = FeedbackTypeName.objects.get(slug='questio')
|
||||
feedback.comments = self.nomcom.encrypt(comment_text)
|
||||
feedback.save()
|
||||
|
@ -659,9 +659,9 @@ class PendingFeedbackForm(forms.ModelForm):
|
|||
model = Feedback
|
||||
fields = ('type', )
|
||||
|
||||
def set_nomcom(self, nomcom, user):
|
||||
def set_nomcom(self, nomcom, person):
|
||||
self.nomcom = nomcom
|
||||
self.user = user
|
||||
self.person = person
|
||||
#self.fields['nominee'] = MultiplePositionNomineeField(nomcom=self.nomcom,
|
||||
#required=True,
|
||||
#widget=forms.SelectMultiple,
|
||||
|
@ -670,7 +670,7 @@ class PendingFeedbackForm(forms.ModelForm):
|
|||
def save(self, commit=True):
|
||||
feedback = super(PendingFeedbackForm, self).save(commit=False)
|
||||
feedback.nomcom = self.nomcom
|
||||
feedback.user = self.user
|
||||
feedback.person = self.person
|
||||
feedback.save()
|
||||
return feedback
|
||||
|
||||
|
@ -700,9 +700,9 @@ class MutableFeedbackForm(forms.ModelForm):
|
|||
model = Feedback
|
||||
fields = ('type', )
|
||||
|
||||
def set_nomcom(self, nomcom, user, instances=None):
|
||||
def set_nomcom(self, nomcom, person, instances=None):
|
||||
self.nomcom = nomcom
|
||||
self.user = user
|
||||
self.person = person
|
||||
instances = instances or []
|
||||
self.feedback_type = None
|
||||
for i in instances:
|
||||
|
@ -782,7 +782,7 @@ class MutableFeedbackForm(forms.ModelForm):
|
|||
nominee=nominee,
|
||||
comments=feedback,
|
||||
nominator_email=nominator_email,
|
||||
user=self.user)
|
||||
person=self.person)
|
||||
return feedback
|
||||
else:
|
||||
feedback.save()
|
||||
|
|
100
ietf/nomcom/migrations/0005_user_to_person.py
Normal file
100
ietf/nomcom/migrations/0005_user_to_person.py
Normal 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",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -7,7 +7,6 @@ import os
|
|||
from django.db import models
|
||||
from django.db.models.signals import post_delete
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.template.loader import render_to_string
|
||||
from django.template.defaultfilters import linebreaks # type: ignore
|
||||
|
||||
|
@ -128,7 +127,7 @@ class Nomination(models.Model):
|
|||
nominee = ForeignKey('Nominee')
|
||||
comments = ForeignKey('Feedback')
|
||||
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)
|
||||
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 '
|
||||
|
@ -299,7 +298,7 @@ class Feedback(models.Model):
|
|||
subject = models.TextField(verbose_name='Subject', blank=True)
|
||||
comments = models.BinaryField(verbose_name='Comments')
|
||||
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)
|
||||
|
||||
objects = FeedbackManager()
|
||||
|
|
|
@ -115,11 +115,11 @@ class NomineePositionResource(ModelResource):
|
|||
api.nomcom.register(NomineePositionResource())
|
||||
|
||||
from ietf.name.resources import FeedbackTypeNameResource
|
||||
from ietf.utils.resources import UserResource
|
||||
from ietf.person.resources import PersonResource
|
||||
class FeedbackResource(ModelResource):
|
||||
nomcom = ToOneField(NomComResource, 'nomcom')
|
||||
type = ToOneField(FeedbackTypeNameResource, 'type', null=True)
|
||||
user = ToOneField(UserResource, 'user', null=True)
|
||||
person = ToOneField(PersonResource, 'person', null=True)
|
||||
positions = ToManyField(PositionResource, 'positions', null=True)
|
||||
nominees = ToManyField(NomineeResource, 'nominees', null=True)
|
||||
class Meta:
|
||||
|
@ -136,18 +136,18 @@ class FeedbackResource(ModelResource):
|
|||
"time": ALL,
|
||||
"nomcom": ALL_WITH_RELATIONS,
|
||||
"type": ALL_WITH_RELATIONS,
|
||||
"user": ALL_WITH_RELATIONS,
|
||||
"person": ALL_WITH_RELATIONS,
|
||||
"positions": ALL_WITH_RELATIONS,
|
||||
"nominees": ALL_WITH_RELATIONS,
|
||||
}
|
||||
api.nomcom.register(FeedbackResource())
|
||||
|
||||
from ietf.utils.resources import UserResource
|
||||
from ietf.person.resources import PersonResource
|
||||
class NominationResource(ModelResource):
|
||||
position = ToOneField(PositionResource, 'position')
|
||||
nominee = ToOneField(NomineeResource, 'nominee')
|
||||
comments = ToOneField(FeedbackResource, 'comments')
|
||||
user = ToOneField(UserResource, 'user', null=True)
|
||||
person = ToOneField(PersonResource, 'person', null=True)
|
||||
class Meta:
|
||||
cache = SimpleCache()
|
||||
queryset = Nomination.objects.all()
|
||||
|
@ -164,7 +164,7 @@ class NominationResource(ModelResource):
|
|||
"position": ALL_WITH_RELATIONS,
|
||||
"nominee": ALL_WITH_RELATIONS,
|
||||
"comments": ALL_WITH_RELATIONS,
|
||||
"user": ALL_WITH_RELATIONS,
|
||||
"person": ALL_WITH_RELATIONS,
|
||||
}
|
||||
api.nomcom.register(NominationResource())
|
||||
|
||||
|
|
|
@ -689,20 +689,16 @@ class NomcomViewsTest(TestCase):
|
|||
self.assertIn('nominee@', outbox[1]['To'])
|
||||
|
||||
|
||||
def nominate_view(self, *args, **kwargs):
|
||||
public = kwargs.pop('public', True)
|
||||
searched_email = kwargs.pop('searched_email', None)
|
||||
nominee_email = kwargs.pop('nominee_email', 'nominee@example.com')
|
||||
def nominate_view(self, public=True, searched_email=None,
|
||||
nominee_email='nominee@example.com',
|
||||
nominator_email=COMMUNITY_USER+EMAIL_DOMAIN,
|
||||
position='IAOC', confirmation=False):
|
||||
|
||||
if not searched_email:
|
||||
searched_email = Email.objects.filter(address=nominee_email).first()
|
||||
if not searched_email:
|
||||
searched_email = EmailFactory(address=nominee_email, primary=True, origin='test')
|
||||
searched_email = Email.objects.filter(address=nominee_email).first() or EmailFactory(address=nominee_email, primary=True, origin='test')
|
||||
if not searched_email.person:
|
||||
searched_email.person = PersonFactory()
|
||||
searched_email.save()
|
||||
nominator_email = kwargs.pop('nominator_email', "%s%s" % (COMMUNITY_USER, EMAIL_DOMAIN))
|
||||
position_name = kwargs.pop('position', 'IAOC')
|
||||
confirmation = kwargs.pop('confirmation', False)
|
||||
|
||||
if public:
|
||||
nominate_url = self.public_nominate_url
|
||||
|
@ -726,7 +722,7 @@ class NomcomViewsTest(TestCase):
|
|||
q = PyQuery(response.content)
|
||||
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 äöåÄÖÅ éáíóú âêîôû ü àèìòù.'
|
||||
candidate_phone = '123456'
|
||||
|
||||
|
@ -764,12 +760,9 @@ class NomcomViewsTest(TestCase):
|
|||
comments=feedback,
|
||||
nominator_email="%s%s" % (COMMUNITY_USER, EMAIL_DOMAIN))
|
||||
|
||||
def nominate_newperson_view(self, *args, **kwargs):
|
||||
public = kwargs.pop('public', True)
|
||||
nominee_email = kwargs.pop('nominee_email', 'nominee@example.com')
|
||||
nominator_email = kwargs.pop('nominator_email', "%s%s" % (COMMUNITY_USER, EMAIL_DOMAIN))
|
||||
position_name = kwargs.pop('position', 'IAOC')
|
||||
confirmation = kwargs.pop('confirmation', False)
|
||||
def nominate_newperson_view(self, public=True, nominee_email='nominee@example.com',
|
||||
nominator_email=COMMUNITY_USER+EMAIL_DOMAIN,
|
||||
position='IAOC', confirmation=False):
|
||||
|
||||
if public:
|
||||
nominate_url = self.public_nominate_newperson_url
|
||||
|
@ -793,7 +786,7 @@ class NomcomViewsTest(TestCase):
|
|||
q = PyQuery(response.content)
|
||||
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_name = 'nominee'
|
||||
comment_text = 'Test nominate view. Comments with accents äöåÄÖÅ éáíóú âêîôû ü àèìòù.'
|
||||
|
@ -847,15 +840,13 @@ class NomcomViewsTest(TestCase):
|
|||
self.access_chair_url(self.add_questionnaire_url)
|
||||
self.add_questionnaire()
|
||||
|
||||
def add_questionnaire(self, *args, **kwargs):
|
||||
public = kwargs.pop('public', False)
|
||||
nominee_email = kwargs.pop('nominee_email', 'nominee@example.com')
|
||||
nominator_email = kwargs.pop('nominator_email', "%s%s" % (COMMUNITY_USER, EMAIL_DOMAIN))
|
||||
position_name = kwargs.pop('position', 'IAOC')
|
||||
def add_questionnaire(self, public=False, nominee_email='nominee@example.com',
|
||||
nominator_email=COMMUNITY_USER+EMAIL_DOMAIN,
|
||||
position='IAOC'):
|
||||
|
||||
self.nominate_view(public=public,
|
||||
nominee_email=nominee_email,
|
||||
position=position_name,
|
||||
position=position,
|
||||
nominator_email=nominator_email)
|
||||
|
||||
response = self.client.get(self.add_questionnaire_url)
|
||||
|
@ -874,7 +865,7 @@ class NomcomViewsTest(TestCase):
|
|||
self.assertEqual(response.status_code, 200)
|
||||
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)
|
||||
|
||||
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.feedback_view(public=False)
|
||||
|
||||
def feedback_view(self, *args, **kwargs):
|
||||
public = kwargs.pop('public', True)
|
||||
nominee_email = kwargs.pop('nominee_email', 'nominee@example.com')
|
||||
nominator_email = kwargs.pop('nominator_email', "%s%s" % (COMMUNITY_USER, EMAIL_DOMAIN))
|
||||
position_name = kwargs.pop('position', 'IAOC')
|
||||
confirmation = kwargs.pop('confirmation', False)
|
||||
def feedback_view(self, public=True, nominee_email='nominee@example.com',
|
||||
nominator_email=COMMUNITY_USER+EMAIL_DOMAIN,
|
||||
position='IAOC', confirmation=False):
|
||||
|
||||
self.nominate_view(public=public,
|
||||
nominee_email=nominee_email,
|
||||
position=position_name,
|
||||
position=position,
|
||||
nominator_email=nominator_email)
|
||||
|
||||
feedback_url = self.public_feedback_url
|
||||
|
@ -956,7 +944,7 @@ class NomcomViewsTest(TestCase):
|
|||
self.assertEqual(response.status_code, 200)
|
||||
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)
|
||||
|
||||
feedback_url += "?nominee=%d&position=%d" % (nominee.id, position.id)
|
||||
|
@ -972,7 +960,7 @@ class NomcomViewsTest(TestCase):
|
|||
comments = 'Test feedback view. Comments with accents äöåÄÖÅ éáíóú âêîôû ü àèìòù.'
|
||||
|
||||
test_data = {'comment_text': comments,
|
||||
'position_name': position.name,
|
||||
'position': position.name,
|
||||
'nominee_name': nominee.email.person.name,
|
||||
'nominee_email': nominee.email.address,
|
||||
'confirmation': confirmation}
|
||||
|
@ -1168,7 +1156,7 @@ class ReminderTest(TestCase):
|
|||
feedback = Feedback.objects.create(nomcom=self.nomcom,
|
||||
comments=self.nomcom.encrypt('some non-empty comments'),
|
||||
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.nominees.add(n)
|
||||
|
||||
|
@ -2192,7 +2180,7 @@ class AcceptingTests(TestCase):
|
|||
self.assertIn('not currently accepting feedback', unicontent(response))
|
||||
|
||||
test_data = {'comment_text': 'junk',
|
||||
'position_name': pos.name,
|
||||
'position': pos.name,
|
||||
'nominee_name': pos.nominee_set.first().email.person.name,
|
||||
'nominee_email': pos.nominee_set.first().email.address,
|
||||
'confirmation': False,
|
||||
|
|
|
@ -88,26 +88,21 @@ def get_year_by_nomcom(nomcom):
|
|||
return m.group(0)
|
||||
|
||||
|
||||
def get_user_email(user):
|
||||
# a user object already has an email field, but we don't want to
|
||||
# overwrite anything that might be there, and we don't know that
|
||||
# what's there is the right thing, so we cache the lookup results in a
|
||||
# separate attribute
|
||||
if not hasattr(user, "_email_cache"):
|
||||
user._email_cache = None
|
||||
if hasattr(user, "person"):
|
||||
emails = user.person.email_set.filter(active=True).order_by('-time')
|
||||
def get_person_email(person):
|
||||
if not hasattr(person, "_email_cache"):
|
||||
person._email_cache = None
|
||||
emails = person.email_set.filter(active=True).order_by('-time')
|
||||
if emails:
|
||||
user._email_cache = emails[0]
|
||||
person._email_cache = emails[0]
|
||||
for email in emails:
|
||||
if email.address.lower() == user.username.lower():
|
||||
user._email_cache = email
|
||||
if email.address.lower() == person.user.username.lower():
|
||||
person._email_cache = email
|
||||
else:
|
||||
try:
|
||||
user._email_cache = Email.objects.get(address=user.username)
|
||||
person._email_cache = Email.objects.get(address=person.user.username)
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
return user._email_cache
|
||||
return person._email_cache
|
||||
|
||||
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()
|
||||
|
|
|
@ -454,23 +454,24 @@ def nominate(request, year, public, newperson):
|
|||
{'nomcom': nomcom,
|
||||
'year': year})
|
||||
|
||||
person = request.user.person
|
||||
if request.method == 'POST':
|
||||
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:
|
||||
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():
|
||||
form.save()
|
||||
messages.success(request, 'Your nomination has been registered. Thank you for the nomination.')
|
||||
if newperson:
|
||||
return redirect('ietf.nomcom.views.%s_nominate' % ('public' if public else 'private'), year=year)
|
||||
else:
|
||||
form = NominateForm(nomcom=nomcom, user=request.user, public=public)
|
||||
form = NominateForm(nomcom=nomcom, person=person, public=public)
|
||||
else:
|
||||
if newperson:
|
||||
form = NominateNewPersonForm(nomcom=nomcom, user=request.user, public=public)
|
||||
form = NominateNewPersonForm(nomcom=nomcom, person=person, public=public)
|
||||
else:
|
||||
form = NominateForm(nomcom=nomcom, user=request.user, public=public)
|
||||
form = NominateForm(nomcom=nomcom, person=person, public=public)
|
||||
|
||||
return render(request, template,
|
||||
{'form': form,
|
||||
|
@ -494,6 +495,7 @@ def feedback(request, year, public):
|
|||
nominee = None
|
||||
position = None
|
||||
topic = None
|
||||
person = request.user.person
|
||||
if nomcom.group.state_id != 'conclude':
|
||||
selected_nominee = request.GET.get('nominee')
|
||||
selected_position = request.GET.get('position')
|
||||
|
@ -505,7 +507,7 @@ def feedback(request, year, public):
|
|||
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']):
|
||||
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()
|
||||
|
||||
if public:
|
||||
|
@ -517,12 +519,12 @@ def feedback(request, year, public):
|
|||
|
||||
if not nomcom.group.has_role(request.user, ['chair','advisor','liaison','member']):
|
||||
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')
|
||||
|
||||
user_comments = Feedback.objects.filter(nomcom=nomcom,
|
||||
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'))
|
||||
counts = dict()
|
||||
for pos,nom in counter:
|
||||
|
@ -572,11 +574,11 @@ def feedback(request, year, public):
|
|||
if request.method == 'POST':
|
||||
if nominee and position:
|
||||
form = FeedbackForm(data=request.POST,
|
||||
nomcom=nomcom, user=request.user,
|
||||
nomcom=nomcom, person=person,
|
||||
public=public, position=position, nominee=nominee)
|
||||
elif topic:
|
||||
form = FeedbackForm(data=request.POST,
|
||||
nomcom=nomcom, user=request.user,
|
||||
nomcom=nomcom, person=person,
|
||||
public=public, topic=topic)
|
||||
else:
|
||||
form = None
|
||||
|
@ -595,10 +597,10 @@ def feedback(request, year, public):
|
|||
pass
|
||||
else:
|
||||
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)
|
||||
elif topic:
|
||||
form = FeedbackForm(nomcom=nomcom, user=request.user, public=public,
|
||||
form = FeedbackForm(nomcom=nomcom, person=person, public=public,
|
||||
topic=topic)
|
||||
else:
|
||||
form = None
|
||||
|
@ -661,6 +663,7 @@ def private_questionnaire(request, year):
|
|||
has_publickey = nomcom.public_key and True or False
|
||||
questionnaire_response = None
|
||||
template = 'nomcom/private_questionnaire.html'
|
||||
person = request.user.person
|
||||
|
||||
if not has_publickey:
|
||||
messages.warning(request, "This Nomcom is not yet accepting questionnaires.")
|
||||
|
@ -680,14 +683,14 @@ def private_questionnaire(request, year):
|
|||
|
||||
if request.method == 'POST':
|
||||
form = QuestionnaireForm(data=request.POST,
|
||||
nomcom=nomcom, user=request.user)
|
||||
nomcom=nomcom, person=person)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, 'The questionnaire response has been registered.')
|
||||
questionnaire_response = force_str(form.cleaned_data['comment_text'])
|
||||
form = QuestionnaireForm(nomcom=nomcom, user=request.user)
|
||||
form = QuestionnaireForm(nomcom=nomcom, person=person)
|
||||
else:
|
||||
form = QuestionnaireForm(nomcom=nomcom, user=request.user)
|
||||
form = QuestionnaireForm(nomcom=nomcom, person=person)
|
||||
|
||||
return render(request, template,
|
||||
{'form': form,
|
||||
|
@ -725,15 +728,13 @@ def process_nomination_status(request, year, nominee_position_id, state, date, h
|
|||
if form.cleaned_data['comments']:
|
||||
# This Feedback object is of type comment instead of nomina in order to not
|
||||
# make answering "who nominated themselves" harder.
|
||||
who = request.user
|
||||
if isinstance(who,AnonymousUser):
|
||||
who = None
|
||||
who = None if isinstance(request.user, AnonymousUser) else request.user.person
|
||||
f = Feedback.objects.create(nomcom = nomcom,
|
||||
author = nominee_position.nominee.email,
|
||||
subject = '%s nomination %s'%(nominee_position.nominee.name(),state),
|
||||
comments = nomcom.encrypt(form.cleaned_data['comments']),
|
||||
type_id = 'comment',
|
||||
user = who,
|
||||
person = who,
|
||||
)
|
||||
f.positions.add(nominee_position.position)
|
||||
f.nominees.add(nominee_position.nominee)
|
||||
|
@ -779,8 +780,9 @@ def view_feedback(request, year):
|
|||
|
||||
sorted_nominees = sorted(nominees,key=lambda x:x.staterank)
|
||||
|
||||
reviewer = request.user.person
|
||||
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 = []
|
||||
for ft in nominee_feedback_types:
|
||||
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} )
|
||||
independent_feedback = [ft.feedback_set.get_by_nomcom(nomcom).count() for ft in independent_feedback_types]
|
||||
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 = []
|
||||
for ft in topic_feedback_types:
|
||||
qs = topic.feedback_set.by_type(ft.slug)
|
||||
|
@ -842,6 +844,7 @@ def view_feedback_pending(request, year):
|
|||
except EmptyPage:
|
||||
feedback_page = paginator.page(paginator.num_pages)
|
||||
extra_step = False
|
||||
person = request.user.person
|
||||
if request.method == 'POST' and request.POST.get('end'):
|
||||
extra_ids = request.POST.get('extra_ids', None)
|
||||
extra_step = True
|
||||
|
@ -850,7 +853,7 @@ def view_feedback_pending(request, year):
|
|||
formset.absolute_max = 2000
|
||||
formset.validate_max = False
|
||||
for form in formset.forms:
|
||||
form.set_nomcom(nomcom, request.user)
|
||||
form.set_nomcom(nomcom, person)
|
||||
if formset.is_valid():
|
||||
formset.save()
|
||||
if extra_ids:
|
||||
|
@ -862,7 +865,7 @@ def view_feedback_pending(request, year):
|
|||
extra.append(feedback)
|
||||
formset = FullFeedbackFormSet(queryset=Feedback.objects.filter(id__in=[i.id for i in extra]))
|
||||
for form in formset.forms:
|
||||
form.set_nomcom(nomcom, request.user, extra)
|
||||
form.set_nomcom(nomcom, person, extra)
|
||||
extra_ids = None
|
||||
else:
|
||||
messages.success(request, 'Feedback saved')
|
||||
|
@ -870,7 +873,7 @@ def view_feedback_pending(request, year):
|
|||
elif request.method == 'POST':
|
||||
formset = FeedbackFormSet(request.POST)
|
||||
for form in formset.forms:
|
||||
form.set_nomcom(nomcom, request.user)
|
||||
form.set_nomcom(nomcom, person)
|
||||
if formset.is_valid():
|
||||
extra = []
|
||||
nominations = []
|
||||
|
@ -890,12 +893,12 @@ def view_feedback_pending(request, year):
|
|||
if nominations:
|
||||
formset = FullFeedbackFormSet(queryset=Feedback.objects.filter(id__in=[i.id for i in nominations]))
|
||||
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])
|
||||
else:
|
||||
formset = FullFeedbackFormSet(queryset=Feedback.objects.filter(id__in=[i.id for i in extra]))
|
||||
for form in formset.forms:
|
||||
form.set_nomcom(nomcom, request.user, extra)
|
||||
form.set_nomcom(nomcom, person, extra)
|
||||
if moved:
|
||||
messages.success(request, '%s messages classified. You must enter more information for the following feedback.' % moved)
|
||||
else:
|
||||
|
@ -904,7 +907,7 @@ def view_feedback_pending(request, year):
|
|||
else:
|
||||
formset = FeedbackFormSet(queryset=feedback_page.object_list)
|
||||
for form in formset.forms:
|
||||
form.set_nomcom(nomcom, request.user)
|
||||
form.set_nomcom(nomcom, person)
|
||||
return render(request, 'nomcom/view_feedback_pending.html',
|
||||
{'year': year,
|
||||
'formset': formset,
|
||||
|
@ -975,13 +978,14 @@ def view_feedback_topic(request, year, topic_id):
|
|||
topic = get_object_or_404(Topic, id=topic_id)
|
||||
nomcom = get_nomcom_by_year(year)
|
||||
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)
|
||||
if last_seen:
|
||||
last_seen.save()
|
||||
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',
|
||||
{'year': year,
|
||||
|
@ -997,7 +1001,7 @@ def view_feedback_nominee(request, year, nominee_id):
|
|||
nomcom = get_nomcom_by_year(year)
|
||||
nominee = get_object_or_404(Nominee, id=nominee_id)
|
||||
feedback_types = FeedbackTypeName.objects.filter(used=True, slug__in=settings.NOMINEE_FEEDBACK_TYPES)
|
||||
|
||||
reviewer = request.user.person
|
||||
if request.method == 'POST':
|
||||
if not nomcom.group.has_role(request.user, ['chair','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,
|
||||
})
|
||||
|
||||
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)
|
||||
if last_seen:
|
||||
last_seen.save()
|
||||
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',
|
||||
{'year': year,
|
||||
|
@ -1322,15 +1326,15 @@ def configuration_help(request, year):
|
|||
@role_required("Nomcom Chair", "Nomcom Advisor")
|
||||
def edit_members(request, year):
|
||||
nomcom = get_nomcom_by_year(year)
|
||||
|
||||
if nomcom.group.state_id=='conclude':
|
||||
permission_denied(request, 'This nomcom is closed.')
|
||||
|
||||
person = request.user.person
|
||||
if request.method=='POST':
|
||||
form = EditMembersForm(nomcom, data=request.POST)
|
||||
if form.is_valid():
|
||||
update_role_set(nomcom.group, 'member', form.cleaned_data['members'], request.user.person)
|
||||
update_role_set(nomcom.group, 'liaison', form.cleaned_data['liaisons'], request.user.person)
|
||||
update_role_set(nomcom.group, 'member', form.cleaned_data['members'], person)
|
||||
update_role_set(nomcom.group, 'liaison', form.cleaned_data['liaisons'], person)
|
||||
return HttpResponseRedirect(reverse('ietf.nomcom.views.private_index',kwargs={'year':year}))
|
||||
else:
|
||||
form = EditMembersForm(nomcom)
|
||||
|
|
|
@ -25,11 +25,11 @@ from ietf.group.models import Group
|
|||
from ietf.nomcom.models import NomCom
|
||||
from ietf.nomcom.test_data import nomcom_test_data
|
||||
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.utils import (merge_persons, determine_merge_order, send_merge_notification,
|
||||
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.utils.test_utils import TestCase, login_testing_unauthorized
|
||||
from ietf.utils.mail import outbox, empty_outbox
|
||||
|
@ -165,6 +165,16 @@ class PersonTests(TestCase):
|
|||
img = Image.open(BytesIO(r.content))
|
||||
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):
|
||||
person = PersonFactory(name="Dr. Jens F. Möller", )
|
||||
|
||||
|
@ -381,13 +391,24 @@ class PersonUtilsTests(TestCase):
|
|||
request.user = user
|
||||
source = PersonFactory()
|
||||
target = PersonFactory()
|
||||
mars = RoleFactory(name_id='chair',group__acronym='mars').group
|
||||
source_id = source.pk
|
||||
source_email = source.email_set.first()
|
||||
source_alias = source.alias_set.first()
|
||||
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())
|
||||
self.assertTrue(source_email in target.email_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(source_user.is_active)
|
||||
|
||||
|
@ -407,24 +428,6 @@ class PersonUtilsTests(TestCase):
|
|||
rs = target.reviewersettings_set.first()
|
||||
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):
|
||||
noroles = PersonFactory()
|
||||
self.assertEqual(get_dots(noroles),[])
|
||||
|
|
|
@ -12,10 +12,11 @@ from django.contrib import admin
|
|||
from django.core.cache import cache
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.models import Q
|
||||
from django.http import Http404
|
||||
|
||||
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
|
||||
|
||||
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()
|
||||
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))
|
||||
reviewer_changes = handle_reviewer_settings(source, target)
|
||||
if reviewer_changes:
|
||||
|
@ -103,7 +118,6 @@ def handle_users(source,target,check_only=False):
|
|||
if source.user and target.user:
|
||||
message = "DATATRACKER LOGIN ACTION: retaining login: {}, removing login: {}".format(target.user,source.user)
|
||||
if not check_only:
|
||||
merge_users(source.user, target.user)
|
||||
syslog.syslog('merge-person-records: deactivating user {}'.format(source.user.username))
|
||||
user = source.user
|
||||
source.user = None
|
||||
|
@ -126,21 +140,6 @@ def move_related_objects(source, target, file, verbose=False):
|
|||
kwargs = { field_name:target }
|
||||
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):
|
||||
'''Check person for duplicate aliases and purge'''
|
||||
seen = []
|
||||
|
@ -248,3 +247,17 @@ def get_dots(person):
|
|||
if roles.filter(group__acronym__startswith='nomcom', name_id__in=('chair','member')).exists():
|
||||
dots.append('nomcom')
|
||||
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
|
||||
|
|
|
@ -8,16 +8,16 @@ from PIL import Image
|
|||
from django.contrib import messages
|
||||
from django.db.models import Q
|
||||
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
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
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.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):
|
||||
|
@ -69,30 +69,14 @@ def ajax_select2_search(request, model_name):
|
|||
|
||||
|
||||
def profile(request, 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)
|
||||
persons = lookup_persons(email_or_name)
|
||||
return render(request, 'person/profile.html', {'persons': persons, 'today': timezone.now()})
|
||||
|
||||
|
||||
def photo(request, email_or_name):
|
||||
if '@' in 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")
|
||||
persons = lookup_persons(email_or_name)
|
||||
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]
|
||||
if not person.photo:
|
||||
raise Http404("No photo found")
|
||||
|
|
43
ietf/review/tasks.py
Normal file
43
ietf/review/tasks.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
# Copyright The IETF Trust 2024, All Rights Reserved
|
||||
#
|
||||
# Celery task definitions
|
||||
#
|
||||
from celery import shared_task
|
||||
|
||||
from ietf.review.utils import (
|
||||
review_assignments_needing_reviewer_reminder, email_reviewer_reminder,
|
||||
review_assignments_needing_secretary_reminder, email_secretary_reminder,
|
||||
send_unavailability_period_ending_reminder, send_reminder_all_open_reviews,
|
||||
send_review_reminder_overdue_assignment, send_reminder_unconfirmed_assignments)
|
||||
from ietf.utils.log import log
|
||||
from ietf.utils.timezone import date_today, DEADLINE_TZINFO
|
||||
|
||||
|
||||
@shared_task
|
||||
def send_review_reminders_task():
|
||||
today = date_today(DEADLINE_TZINFO)
|
||||
|
||||
for assignment in review_assignments_needing_reviewer_reminder(today):
|
||||
email_reviewer_reminder(assignment)
|
||||
log("Emailed reminder to {} for review of {} in {} (req. id {})".format(assignment.reviewer.address, assignment.review_request.doc_id, assignment.review_request.team.acronym, assignment.review_request.pk))
|
||||
|
||||
for assignment, secretary_role in review_assignments_needing_secretary_reminder(today):
|
||||
email_secretary_reminder(assignment, secretary_role)
|
||||
review_req = assignment.review_request
|
||||
log("Emailed reminder to {} for review of {} in {} (req. id {})".format(secretary_role.email.address, review_req.doc_id, review_req.team.acronym, review_req.pk))
|
||||
|
||||
period_end_reminders_sent = send_unavailability_period_ending_reminder(today)
|
||||
for msg in period_end_reminders_sent:
|
||||
log(msg)
|
||||
|
||||
overdue_reviews_reminders_sent = send_review_reminder_overdue_assignment(today)
|
||||
for msg in overdue_reviews_reminders_sent:
|
||||
log(msg)
|
||||
|
||||
open_reviews_reminders_sent = send_reminder_all_open_reviews(today)
|
||||
for msg in open_reviews_reminders_sent:
|
||||
log(msg)
|
||||
|
||||
unconfirmed_assignment_reminders_sent = send_reminder_unconfirmed_assignments(today)
|
||||
for msg in unconfirmed_assignment_reminders_sent:
|
||||
log(msg)
|
|
@ -1,9 +1,11 @@
|
|||
# Copyright The IETF Trust 2019-2020, All Rights Reserved
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
import mock
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
from pyquery import PyQuery
|
||||
|
||||
from ietf.group.factories import RoleFactory
|
||||
from ietf.doc.factories import WgDraftFactory
|
||||
from ietf.utils.mail import empty_outbox, get_payload_text, outbox
|
||||
|
@ -13,6 +15,7 @@ from ietf.utils.timezone import date_today, datetime_from_date
|
|||
from .factories import ReviewAssignmentFactory, ReviewRequestFactory, ReviewerSettingsFactory
|
||||
from .mailarch import hash_list_message_id
|
||||
from .models import ReviewerSettings, ReviewSecretarySettings, ReviewTeamSettings, UnavailablePeriod
|
||||
from .tasks import send_review_reminders_task
|
||||
from .utils import (email_secretary_reminder, review_assignments_needing_secretary_reminder,
|
||||
email_reviewer_reminder, review_assignments_needing_reviewer_reminder,
|
||||
send_reminder_unconfirmed_assignments, send_review_reminder_overdue_assignment,
|
||||
|
@ -550,3 +553,66 @@ class AddReviewCommentTestCase(TestCase):
|
|||
# But can't have the comment we are goint to add.
|
||||
self.assertContains(r, 'This is a test.')
|
||||
|
||||
|
||||
class TaskTests(TestCase):
|
||||
# hyaaa it's mockzilla
|
||||
@mock.patch("ietf.review.tasks.date_today")
|
||||
@mock.patch("ietf.review.tasks.review_assignments_needing_reviewer_reminder")
|
||||
@mock.patch("ietf.review.tasks.email_reviewer_reminder")
|
||||
@mock.patch("ietf.review.tasks.review_assignments_needing_secretary_reminder")
|
||||
@mock.patch("ietf.review.tasks.email_secretary_reminder")
|
||||
@mock.patch("ietf.review.tasks.send_unavailability_period_ending_reminder")
|
||||
@mock.patch("ietf.review.tasks.send_reminder_all_open_reviews")
|
||||
@mock.patch("ietf.review.tasks.send_review_reminder_overdue_assignment")
|
||||
@mock.patch("ietf.review.tasks.send_reminder_unconfirmed_assignments")
|
||||
def test_send_review_reminders_task(
|
||||
self,
|
||||
mock_send_reminder_unconfirmed_assignments,
|
||||
mock_send_review_reminder_overdue_assignment,
|
||||
mock_send_reminder_all_open_reviews,
|
||||
mock_send_unavailability_period_ending_reminder,
|
||||
mock_email_secretary_reminder,
|
||||
mock_review_assignments_needing_secretary_reminder,
|
||||
mock_email_reviewer_reminder,
|
||||
mock_review_assignments_needing_reviewer_reminder,
|
||||
mock_date_today,
|
||||
):
|
||||
"""Test that send_review_reminders calls functions correctly
|
||||
|
||||
Does not test individual methods, just that they are called as expected.
|
||||
"""
|
||||
mock_today = object()
|
||||
assignment = ReviewAssignmentFactory()
|
||||
secretary_role = RoleFactory(name_id="secr")
|
||||
|
||||
mock_date_today.return_value = mock_today
|
||||
mock_review_assignments_needing_reviewer_reminder.return_value = [assignment]
|
||||
mock_review_assignments_needing_secretary_reminder.return_value = [[assignment, secretary_role]]
|
||||
mock_send_unavailability_period_ending_reminder.return_value = ["pretending I sent a period end reminder"]
|
||||
mock_send_review_reminder_overdue_assignment.return_value = ["pretending I sent an overdue reminder"]
|
||||
mock_send_reminder_all_open_reviews.return_value = ["pretending I sent an open review reminder"]
|
||||
mock_send_reminder_unconfirmed_assignments.return_value = ["pretending I sent an unconfirmed reminder"]
|
||||
|
||||
send_review_reminders_task()
|
||||
|
||||
self.assertEqual(mock_review_assignments_needing_reviewer_reminder.call_count, 1)
|
||||
self.assertEqual(mock_review_assignments_needing_reviewer_reminder.call_args[0], (mock_today,))
|
||||
self.assertEqual(mock_email_reviewer_reminder.call_count, 1)
|
||||
self.assertEqual(mock_email_reviewer_reminder.call_args[0], (assignment,))
|
||||
|
||||
self.assertEqual(mock_review_assignments_needing_secretary_reminder.call_count, 1)
|
||||
self.assertEqual(mock_review_assignments_needing_secretary_reminder.call_args[0], (mock_today,))
|
||||
self.assertEqual(mock_email_secretary_reminder.call_count, 1)
|
||||
self.assertEqual(mock_email_secretary_reminder.call_args[0], (assignment, secretary_role))
|
||||
|
||||
self.assertEqual(mock_send_unavailability_period_ending_reminder.call_count, 1)
|
||||
self.assertEqual(mock_send_unavailability_period_ending_reminder.call_args[0], (mock_today,))
|
||||
|
||||
self.assertEqual(mock_send_review_reminder_overdue_assignment.call_count, 1)
|
||||
self.assertEqual(mock_send_review_reminder_overdue_assignment.call_args[0], (mock_today,))
|
||||
|
||||
self.assertEqual(mock_send_reminder_all_open_reviews.call_count, 1)
|
||||
self.assertEqual(mock_send_reminder_all_open_reviews.call_args[0], (mock_today,))
|
||||
|
||||
self.assertEqual(mock_send_reminder_unconfirmed_assignments.call_count, 1)
|
||||
self.assertEqual(mock_send_reminder_unconfirmed_assignments.call_args[0], (mock_today,))
|
||||
|
|
|
@ -7,7 +7,6 @@ from django.urls import reverse
|
|||
|
||||
from ietf.ietfauth.utils import role_required
|
||||
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
|
||||
|
||||
|
||||
|
@ -179,7 +178,6 @@ def edit(request, id):
|
|||
if 'user' in person_form.changed_data and person_form.initial['user']:
|
||||
try:
|
||||
source = User.objects.get(username__iexact=person_form.initial['user'])
|
||||
merge_users(source, person_form.cleaned_data['user'])
|
||||
source.is_active = False
|
||||
source.save()
|
||||
except User.DoesNotExist:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright The IETF Trust 2007-2023, All Rights Reserved
|
||||
# Copyright The IETF Trust 2007-2024, All Rights Reserved
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
|
@ -169,8 +169,8 @@ if SERVER_MODE != 'production' and SERVE_CDN_FILES_LOCALLY_IN_DEV_MODE:
|
|||
STATIC_URL = "/static/"
|
||||
STATIC_ROOT = os.path.abspath(BASE_DIR + "/../static/")
|
||||
else:
|
||||
STATIC_URL = "https://www.ietf.org/lib/dt/%s/"%__version__
|
||||
STATIC_ROOT = "/a/www/www6s/lib/dt/%s/"%__version__
|
||||
STATIC_URL = "https://static.ietf.org/lib/%s/"%__version__
|
||||
# Intentionally not setting STATIC_ROOT - see django/django (the default is None)
|
||||
|
||||
# List of finder classes that know how to find static files in
|
||||
# various locations.
|
||||
|
@ -1153,6 +1153,12 @@ CELERY_BROKER_URL = 'amqp://mq/'
|
|||
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
|
||||
CELERY_BEAT_SYNC_EVERY = 1 # update DB after every event
|
||||
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True # the default, but setting it squelches a warning
|
||||
# Use a result backend so we can chain tasks. This uses the rpc backend, see
|
||||
# https://docs.celeryq.dev/en/stable/userguide/tasks.html#rpc-result-backend-rabbitmq-qpid
|
||||
# Results can be retrieved only once and only by the caller of the task. Results will be
|
||||
# lost if the message broker restarts.
|
||||
CELERY_RESULT_BACKEND = 'rpc://' # sends a msg via the msg broker
|
||||
CELERY_TASK_IGNORE_RESULT = True # ignore results unless specifically enabled for a task
|
||||
|
||||
# Meetecho API setup: Uncomment this and provide real credentials to enable
|
||||
# Meetecho conference creation for interim session requests
|
||||
|
|
|
@ -9,7 +9,7 @@ from django.utils import timezone
|
|||
import debug # pyflakes:ignore
|
||||
|
||||
from ietf.meeting.models import Meeting
|
||||
from ietf.stats.utils import get_meeting_registration_data
|
||||
from ietf.stats.utils import fetch_attendance_from_meetings
|
||||
|
||||
logtag = __name__.split('.')[-1]
|
||||
logname = "user.log"
|
||||
|
@ -36,11 +36,11 @@ class Command(BaseCommand):
|
|||
else:
|
||||
raise CommandError("Please use one of --meeting, --all or --latest")
|
||||
|
||||
for meeting in meetings:
|
||||
added, processed, total = get_meeting_registration_data(meeting)
|
||||
msg = "Fetched data for meeting %3s: %4d processed, %4d added, %4d in table" % (meeting.number, processed, added, total)
|
||||
for meeting, stats in zip(meetings, fetch_attendance_from_meetings(meetings)):
|
||||
msg = "Fetched data for meeting {:>3}: {:4d} processed, {:4d} added, {:4d} in table".format(
|
||||
meeting.number, stats.processed, stats.added, stats.total
|
||||
)
|
||||
if self.stdout.isatty():
|
||||
self.stdout.write(msg+'\n') # make debugging a bit easier
|
||||
else:
|
||||
syslog.syslog(msg)
|
||||
|
||||
|
|
27
ietf/stats/tasks.py
Normal file
27
ietf/stats/tasks.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Copyright The IETF Trust 2024, All Rights Reserved
|
||||
#
|
||||
# Celery task definitions
|
||||
#
|
||||
from celery import shared_task
|
||||
from django.utils import timezone
|
||||
|
||||
from ietf.meeting.models import Meeting
|
||||
from ietf.stats.utils import fetch_attendance_from_meetings
|
||||
from ietf.utils import log
|
||||
|
||||
|
||||
@shared_task
|
||||
def fetch_meeting_attendance_task():
|
||||
# fetch most recent two meetings
|
||||
meetings = Meeting.objects.filter(type="ietf", date__lte=timezone.now()).order_by("-date")[:2]
|
||||
try:
|
||||
stats = fetch_attendance_from_meetings(meetings)
|
||||
except RuntimeError as err:
|
||||
log.log(f"Error in fetch_meeting_attendance_task: {err}")
|
||||
else:
|
||||
for meeting, meeting_stats in zip(meetings, stats):
|
||||
log.log(
|
||||
"Fetched data for meeting {:>3}: {:4d} processed, {:4d} added, {:4d} in table".format(
|
||||
meeting.number, meeting_stats.processed, meeting_stats.added, meeting_stats.total
|
||||
)
|
||||
)
|
|
@ -29,7 +29,8 @@ from ietf.name.models import FormalLanguageName, DocRelationshipName, CountryNam
|
|||
from ietf.review.factories import ReviewRequestFactory, ReviewerSettingsFactory, ReviewAssignmentFactory
|
||||
from ietf.stats.models import MeetingRegistration, CountryAlias
|
||||
from ietf.stats.factories import MeetingRegistrationFactory
|
||||
from ietf.stats.utils import get_meeting_registration_data
|
||||
from ietf.stats.tasks import fetch_meeting_attendance_task
|
||||
from ietf.stats.utils import get_meeting_registration_data, FetchStats, fetch_attendance_from_meetings
|
||||
from ietf.utils.timezone import date_today
|
||||
|
||||
|
||||
|
@ -300,3 +301,48 @@ class StatisticsTests(TestCase):
|
|||
get_meeting_registration_data(meeting)
|
||||
query = MeetingRegistration.objects.all()
|
||||
self.assertEqual(query.count(), 2)
|
||||
|
||||
@patch("ietf.stats.utils.get_meeting_registration_data")
|
||||
def test_fetch_attendance_from_meetings(self, mock_get_mtg_reg_data):
|
||||
mock_meetings = [object(), object(), object()]
|
||||
mock_get_mtg_reg_data.side_effect = (
|
||||
(1, 2, 3),
|
||||
(4, 5, 6),
|
||||
(7, 8, 9),
|
||||
)
|
||||
stats = fetch_attendance_from_meetings(mock_meetings)
|
||||
self.assertEqual(
|
||||
[mock_get_mtg_reg_data.call_args_list[n][0][0] for n in range(3)],
|
||||
mock_meetings,
|
||||
)
|
||||
self.assertEqual(
|
||||
stats,
|
||||
[
|
||||
FetchStats(1, 2, 3),
|
||||
FetchStats(4, 5, 6),
|
||||
FetchStats(7, 8, 9),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class TaskTests(TestCase):
|
||||
@patch("ietf.stats.tasks.fetch_attendance_from_meetings")
|
||||
def test_fetch_meeting_attendance_task(self, mock_fetch_attendance):
|
||||
today = date_today()
|
||||
meetings = [
|
||||
MeetingFactory(type_id="ietf", date=today - datetime.timedelta(days=1)),
|
||||
MeetingFactory(type_id="ietf", date=today - datetime.timedelta(days=2)),
|
||||
MeetingFactory(type_id="ietf", date=today - datetime.timedelta(days=3)),
|
||||
]
|
||||
mock_fetch_attendance.return_value = [FetchStats(1,2,3), FetchStats(1,2,3)]
|
||||
|
||||
fetch_meeting_attendance_task()
|
||||
self.assertEqual(mock_fetch_attendance.call_count, 1)
|
||||
self.assertCountEqual(mock_fetch_attendance.call_args[0][0], meetings[0:2])
|
||||
|
||||
# test handling of RuntimeError
|
||||
mock_fetch_attendance.reset_mock()
|
||||
mock_fetch_attendance.side_effect = RuntimeError
|
||||
fetch_meeting_attendance_task()
|
||||
self.assertTrue(mock_fetch_attendance.called)
|
||||
# Good enough that we got here without raising an exception
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
import re
|
||||
import requests
|
||||
from collections import defaultdict
|
||||
from collections import defaultdict, namedtuple
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
|
@ -382,3 +382,13 @@ def find_meetingregistration_person_issues(meetings=None):
|
|||
summary.no_email.add(f'{mr} ({mr.pk}) provides no email address')
|
||||
|
||||
return summary
|
||||
|
||||
|
||||
FetchStats = namedtuple("FetchStats", "added processed total")
|
||||
|
||||
|
||||
def fetch_attendance_from_meetings(meetings):
|
||||
stats = [
|
||||
FetchStats(*get_meeting_registration_data(meeting)) for meeting in meetings
|
||||
]
|
||||
return stats
|
||||
|
|
|
@ -336,7 +336,7 @@ def parse_index(response):
|
|||
|
||||
|
||||
def update_docs_from_rfc_index(
|
||||
index_data, errata_data, skip_older_than_date=None
|
||||
index_data, errata_data, skip_older_than_date: Optional[datetime.date] = None
|
||||
) -> Iterator[tuple[int, list[str], Document, bool]]:
|
||||
"""Given parsed data from the RFC Editor index, update the documents in the database
|
||||
|
||||
|
@ -405,7 +405,8 @@ def update_docs_from_rfc_index(
|
|||
abstract,
|
||||
) in index_data:
|
||||
if skip_older_than_date and rfc_published_date < skip_older_than_date:
|
||||
# speed up the process by skipping old entries
|
||||
# speed up the process by skipping old entries (n.b., the comparison above is a
|
||||
# lexical comparison between "YYYY-MM-DD"-formatted dates)
|
||||
continue
|
||||
|
||||
# we assume two things can happen: we get a new RFC, or an
|
||||
|
@ -462,6 +463,14 @@ def update_docs_from_rfc_index(
|
|||
doc.set_state(rfc_published_state)
|
||||
if draft:
|
||||
doc.formal_languages.set(draft.formal_languages.all())
|
||||
for author in draft.documentauthor_set.all():
|
||||
# Copy the author but point at the new doc.
|
||||
# See https://docs.djangoproject.com/en/4.2/topics/db/queries/#copying-model-instances
|
||||
author.pk = None
|
||||
author.id = None
|
||||
author._state.adding = True
|
||||
author.document = doc
|
||||
author.save()
|
||||
|
||||
if draft:
|
||||
draft_events = []
|
||||
|
|
67
ietf/sync/tasks.py
Normal file
67
ietf/sync/tasks.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
# Copyright The IETF Trust 2024, All Rights Reserved
|
||||
#
|
||||
# Celery task definitions
|
||||
#
|
||||
import datetime
|
||||
import io
|
||||
import requests
|
||||
from celery import shared_task
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from ietf.sync.rfceditor import MIN_ERRATA_RESULTS, MIN_INDEX_RESULTS, parse_index, update_docs_from_rfc_index
|
||||
from ietf.utils import log
|
||||
from ietf.utils.timezone import date_today
|
||||
|
||||
|
||||
@shared_task
|
||||
def rfc_editor_index_update_task(full_index=False):
|
||||
"""Update metadata from the RFC index
|
||||
|
||||
Default is to examine only changes in the past 365 days. Call with full_index=True to update
|
||||
the full RFC index.
|
||||
|
||||
According to comments on the original script, a year's worth took about 20s on production as of
|
||||
August 2022
|
||||
|
||||
The original rfc-editor-index-update script had a long-disabled provision for running the
|
||||
rebuild_reference_relations scripts after the update. That has not been brought over
|
||||
at all because it should be implemented as its own task if it is needed.
|
||||
"""
|
||||
skip_date = None if full_index else date_today() - datetime.timedelta(days=365)
|
||||
log.log(
|
||||
"Updating document metadata from RFC index going back to {since}, from {url}".format(
|
||||
since=skip_date if skip_date is not None else "the beginning",
|
||||
url=settings.RFC_EDITOR_INDEX_URL,
|
||||
)
|
||||
)
|
||||
try:
|
||||
response = requests.get(
|
||||
settings.RFC_EDITOR_INDEX_URL,
|
||||
timeout=30, # seconds
|
||||
)
|
||||
except requests.Timeout as exc:
|
||||
log.log(f'GET request timed out retrieving RFC editor index: {exc}')
|
||||
return # failed
|
||||
rfc_index_xml = response.text
|
||||
index_data = parse_index(io.StringIO(rfc_index_xml))
|
||||
try:
|
||||
response = requests.get(
|
||||
settings.RFC_EDITOR_ERRATA_JSON_URL,
|
||||
timeout=30, # seconds
|
||||
)
|
||||
except requests.Timeout as exc:
|
||||
log.log(f'GET request timed out retrieving RFC editor errata: {exc}')
|
||||
return # failed
|
||||
errata_data = response.json()
|
||||
if len(index_data) < MIN_INDEX_RESULTS:
|
||||
log.log("Not enough index entries, only %s" % len(index_data))
|
||||
return # failed
|
||||
if len(errata_data) < MIN_ERRATA_RESULTS:
|
||||
log.log("Not enough errata entries, only %s" % len(errata_data))
|
||||
return # failed
|
||||
for rfc_number, changes, doc, rfc_published in update_docs_from_rfc_index(
|
||||
index_data, errata_data, skip_older_than_date=skip_date
|
||||
):
|
||||
for c in changes:
|
||||
log.log("RFC%s, %s: %s" % (rfc_number, doc.name, c))
|
|
@ -8,19 +8,23 @@ import json
|
|||
import datetime
|
||||
import mock
|
||||
import quopri
|
||||
import requests
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from django.conf import settings
|
||||
from django.urls import reverse as urlreverse
|
||||
from django.utils import timezone
|
||||
from django.test.utils import override_settings
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
from ietf.doc.factories import WgDraftFactory, RfcFactory
|
||||
from ietf.doc.factories import WgDraftFactory, RfcFactory, DocumentAuthorFactory
|
||||
from ietf.doc.models import Document, DocEvent, DeletedEvent, DocTagName, RelatedDocument, State, StateDocEvent
|
||||
from ietf.doc.utils import add_state_change_event
|
||||
from ietf.group.factories import GroupFactory
|
||||
from ietf.person.models import Person
|
||||
from ietf.sync import iana, rfceditor
|
||||
from ietf.sync import iana, rfceditor, tasks
|
||||
from ietf.utils.mail import outbox, empty_outbox
|
||||
from ietf.utils.test_utils import login_testing_unauthorized
|
||||
from ietf.utils.test_utils import TestCase
|
||||
|
@ -235,6 +239,7 @@ class RFCSyncTests(TestCase):
|
|||
external_url="http://my-external-url.example.com",
|
||||
note="this is a note",
|
||||
)
|
||||
DocumentAuthorFactory.create_batch(2, document=draft_doc)
|
||||
draft_doc.action_holders.add(draft_doc.ad) # not normally set, but add to be sure it's cleared
|
||||
|
||||
RfcFactory(rfc_number=123)
|
||||
|
@ -378,6 +383,7 @@ class RFCSyncTests(TestCase):
|
|||
|
||||
rfc_doc = Document.objects.filter(rfc_number=1234, type_id="rfc").first()
|
||||
self.assertIsNotNone(rfc_doc, "RFC document should have been created")
|
||||
self.assertEqual(rfc_doc.authors(), draft_doc.authors())
|
||||
rfc_events = rfc_doc.docevent_set.all()
|
||||
self.assertEqual(len(rfc_events), 8)
|
||||
expected_events = [
|
||||
|
@ -672,3 +678,129 @@ class RFCEditorUndoTests(TestCase):
|
|||
|
||||
e.content_type.model_class().objects.create(**json.loads(e.json))
|
||||
self.assertTrue(StateDocEvent.objects.filter(desc="First", doc=draft))
|
||||
|
||||
|
||||
class TaskTests(TestCase):
|
||||
@override_settings(
|
||||
RFC_EDITOR_INDEX_URL="https://rfc-editor.example.com/index/",
|
||||
RFC_EDITOR_ERRATA_JSON_URL="https://rfc-editor.example.com/errata/",
|
||||
)
|
||||
@mock.patch("ietf.sync.tasks.update_docs_from_rfc_index")
|
||||
@mock.patch("ietf.sync.tasks.parse_index")
|
||||
@mock.patch("ietf.sync.tasks.requests.get")
|
||||
def test_rfc_editor_index_update_task(
|
||||
self, requests_get_mock, parse_index_mock, update_docs_mock
|
||||
) -> None: # the annotation here prevents mypy from complaining about annotation-unchecked
|
||||
"""rfc_editor_index_update_task calls helpers correctly
|
||||
|
||||
This tests that data flow is as expected. Assumes the individual helpers are
|
||||
separately tested to function correctly.
|
||||
"""
|
||||
@dataclass
|
||||
class MockIndexData:
|
||||
"""Mock index item that claims to be a specified length"""
|
||||
length: int
|
||||
|
||||
def __len__(self):
|
||||
return self.length
|
||||
|
||||
@dataclass
|
||||
class MockResponse:
|
||||
"""Mock object that contains text and json() that claims to be a specified length"""
|
||||
text: str
|
||||
json_length: int = 0
|
||||
|
||||
def json(self):
|
||||
return MockIndexData(length=self.json_length)
|
||||
|
||||
# Response objects
|
||||
index_response = MockResponse(text="this is the index")
|
||||
errata_response = MockResponse(
|
||||
text="these are the errata", json_length=rfceditor.MIN_ERRATA_RESULTS
|
||||
)
|
||||
rfc = RfcFactory()
|
||||
|
||||
# Test with full_index = False
|
||||
requests_get_mock.side_effect = (index_response, errata_response) # will step through these
|
||||
parse_index_mock.return_value = MockIndexData(length=rfceditor.MIN_INDEX_RESULTS)
|
||||
update_docs_mock.return_value = (
|
||||
(rfc.rfc_number, ("something changed",), rfc, False),
|
||||
)
|
||||
|
||||
tasks.rfc_editor_index_update_task(full_index=False)
|
||||
|
||||
# Check parse_index() call
|
||||
self.assertTrue(parse_index_mock.called)
|
||||
(parse_index_args, _) = parse_index_mock.call_args
|
||||
self.assertEqual(
|
||||
parse_index_args[0].read(), # arg is a StringIO
|
||||
"this is the index",
|
||||
"parse_index is called with the index text in a StringIO",
|
||||
)
|
||||
|
||||
# Check update_docs_from_rfc_index call
|
||||
self.assertTrue(update_docs_mock.called)
|
||||
(update_docs_args, update_docs_kwargs) = update_docs_mock.call_args
|
||||
self.assertEqual(
|
||||
update_docs_args, (parse_index_mock.return_value, errata_response.json())
|
||||
)
|
||||
self.assertIsNotNone(update_docs_kwargs["skip_older_than_date"])
|
||||
|
||||
# Test again with full_index = True
|
||||
requests_get_mock.reset_mock()
|
||||
parse_index_mock.reset_mock()
|
||||
update_docs_mock.reset_mock()
|
||||
requests_get_mock.side_effect = (index_response, errata_response) # will step through these
|
||||
tasks.rfc_editor_index_update_task(full_index=True)
|
||||
|
||||
# Check parse_index() call
|
||||
self.assertTrue(parse_index_mock.called)
|
||||
(parse_index_args, _) = parse_index_mock.call_args
|
||||
self.assertEqual(
|
||||
parse_index_args[0].read(), # arg is a StringIO
|
||||
"this is the index",
|
||||
"parse_index is called with the index text in a StringIO",
|
||||
)
|
||||
|
||||
# Check update_docs_from_rfc_index call
|
||||
self.assertTrue(update_docs_mock.called)
|
||||
(update_docs_args, update_docs_kwargs) = update_docs_mock.call_args
|
||||
self.assertEqual(
|
||||
update_docs_args, (parse_index_mock.return_value, errata_response.json())
|
||||
)
|
||||
self.assertIsNone(update_docs_kwargs["skip_older_than_date"])
|
||||
|
||||
# Test error handling
|
||||
requests_get_mock.reset_mock()
|
||||
parse_index_mock.reset_mock()
|
||||
update_docs_mock.reset_mock()
|
||||
requests_get_mock.side_effect = requests.Timeout # timeout on every get()
|
||||
tasks.rfc_editor_index_update_task(full_index=False)
|
||||
self.assertFalse(parse_index_mock.called)
|
||||
self.assertFalse(update_docs_mock.called)
|
||||
|
||||
requests_get_mock.reset_mock()
|
||||
parse_index_mock.reset_mock()
|
||||
update_docs_mock.reset_mock()
|
||||
requests_get_mock.side_effect = [index_response, requests.Timeout] # timeout second get()
|
||||
tasks.rfc_editor_index_update_task(full_index=False)
|
||||
self.assertFalse(update_docs_mock.called)
|
||||
|
||||
requests_get_mock.reset_mock()
|
||||
parse_index_mock.reset_mock()
|
||||
update_docs_mock.reset_mock()
|
||||
requests_get_mock.side_effect = [index_response, errata_response]
|
||||
# feed in an index that is too short
|
||||
parse_index_mock.return_value = MockIndexData(length=rfceditor.MIN_INDEX_RESULTS - 1)
|
||||
tasks.rfc_editor_index_update_task(full_index=False)
|
||||
self.assertTrue(parse_index_mock.called)
|
||||
self.assertFalse(update_docs_mock.called)
|
||||
|
||||
requests_get_mock.reset_mock()
|
||||
parse_index_mock.reset_mock()
|
||||
update_docs_mock.reset_mock()
|
||||
requests_get_mock.side_effect = [index_response, errata_response]
|
||||
errata_response.json_length = rfceditor.MIN_ERRATA_RESULTS - 1 # too short
|
||||
parse_index_mock.return_value = MockIndexData(length=rfceditor.MIN_INDEX_RESULTS)
|
||||
tasks.rfc_editor_index_update_task(full_index=False)
|
||||
self.assertFalse(update_docs_mock.called)
|
||||
|
|
|
@ -3,18 +3,18 @@
|
|||
<div class="btn-group" role="group" aria-labelledby="list-feeds">
|
||||
<a class="btn btn-primary"
|
||||
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
|
||||
</a>
|
||||
<a class="btn btn-primary"
|
||||
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
|
||||
</a>
|
||||
</div>
|
||||
{% if clist.pk != None %}
|
||||
<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>
|
||||
{% if subscribed %}
|
||||
Change subscription
|
||||
|
@ -24,7 +24,7 @@
|
|||
</a>
|
||||
{% endif %}
|
||||
<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
|
||||
</a>
|
||||
</div>
|
|
@ -12,7 +12,7 @@
|
|||
{% bootstrap_messages %}
|
||||
{% if can_manage_list %}
|
||||
<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>
|
||||
Manage list
|
||||
</a>
|
||||
|
|
|
@ -699,21 +699,21 @@
|
|||
</div>
|
||||
{% 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 %}"
|
||||
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">
|
||||
<i class="bi bi-bookmark-check-fill">
|
||||
</i>
|
||||
Untrack
|
||||
</a>
|
||||
<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">
|
||||
<i class="bi bi-bookmark">
|
||||
</i>
|
||||
Track
|
||||
</a>
|
||||
{% 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 %}"
|
||||
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">
|
||||
|
@ -721,7 +721,7 @@
|
|||
</i>
|
||||
Remove review wishes
|
||||
</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 }}"
|
||||
title="Add to your review wishes">
|
||||
<i class="bi bi-chat-left-heart">
|
||||
|
|
|
@ -129,14 +129,14 @@
|
|||
</a>
|
||||
{% if user.is_authenticated %}
|
||||
<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">
|
||||
<i class="bi bi-bookmark-check-fill">
|
||||
</i>
|
||||
Untrack
|
||||
</a>
|
||||
<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">
|
||||
<i class="bi bi-bookmark">
|
||||
</i>
|
||||
|
|
|
@ -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 %}>
|
||||
<td class="bg-transparent">
|
||||
{% 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 %}"
|
||||
aria-label="Remove from your personal I-D list"
|
||||
title="Remove from your personal I-D list">
|
||||
<i class="bi bi-bookmark-check-fill"></i>
|
||||
</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 %}"
|
||||
aria-label="Add to your personal I-D list"
|
||||
title="Add to your personal I-D list">
|
||||
|
@ -23,14 +23,14 @@
|
|||
</a>
|
||||
<br>
|
||||
{% 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 %}"
|
||||
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"
|
||||
title="Remove from your review wishes for all teams">
|
||||
<i class="bi bi-chat-left-heart-fill"></i>
|
||||
</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 }}"
|
||||
aria-label="Add to your review wishes"
|
||||
title="Add to your review wishes">
|
||||
|
|
139
ietf/utils/management/commands/periodic_tasks.py
Normal file
139
ietf/utils/management/commands/periodic_tasks.py
Normal file
|
@ -0,0 +1,139 @@
|
|||
# Copyright The IETF Trust 2024, All Rights Reserved
|
||||
import json
|
||||
from django_celery_beat.models import CrontabSchedule, PeriodicTask
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
CRONTAB_DEFS = {
|
||||
"daily": {
|
||||
"minute": "5",
|
||||
"hour": "0",
|
||||
"day_of_week": "*",
|
||||
"day_of_month": "*",
|
||||
"month_of_year": "*",
|
||||
},
|
||||
"hourly": {
|
||||
"minute": "5",
|
||||
"hour": "*",
|
||||
"day_of_week": "*",
|
||||
"day_of_month": "*",
|
||||
"month_of_year": "*",
|
||||
},
|
||||
"every_15m": {
|
||||
"minute": "*/15",
|
||||
"hour": "*",
|
||||
"day_of_week": "*",
|
||||
"day_of_month": "*",
|
||||
"month_of_year": "*",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Manage periodic tasks"""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("--create-default", action="store_true")
|
||||
parser.add_argument("--enable", type=int, action="append")
|
||||
parser.add_argument("--disable", type=int, action="append")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.crontabs = self.get_or_create_crontabs()
|
||||
if options["create_default"]:
|
||||
self.create_default_tasks()
|
||||
if options["enable"]:
|
||||
self.enable_tasks(options["enable"])
|
||||
if options["disable"]:
|
||||
self.disable_tasks(options["disable"])
|
||||
self.show_tasks()
|
||||
|
||||
def get_or_create_crontabs(self):
|
||||
crontabs = {}
|
||||
for label, definition in CRONTAB_DEFS.items():
|
||||
crontabs[label], _ = CrontabSchedule.objects.get_or_create(**definition)
|
||||
return crontabs
|
||||
|
||||
def create_default_tasks(self):
|
||||
PeriodicTask.objects.get_or_create(
|
||||
name="Send scheduled mail",
|
||||
task="ietf.message.tasks.send_scheduled_mail_task",
|
||||
defaults=dict(
|
||||
enabled=False,
|
||||
crontab=self.crontabs["every_15m"],
|
||||
description="Send mail scheduled to go out at certain times"
|
||||
),
|
||||
)
|
||||
|
||||
PeriodicTask.objects.get_or_create(
|
||||
name="Partial sync with RFC Editor index",
|
||||
task="ietf.sync.tasks.rfc_editor_index_update_task",
|
||||
kwargs=json.dumps(dict(full_index=False)),
|
||||
defaults=dict(
|
||||
enabled=False,
|
||||
crontab=self.crontabs["every_15m"],
|
||||
description=(
|
||||
"Reparse the last _year_ of RFC index entries until "
|
||||
"https://github.com/ietf-tools/datatracker/issues/3734 is addressed. "
|
||||
"This takes about 20s on production as of 2022-08-11."
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
PeriodicTask.objects.get_or_create(
|
||||
name="Full sync with RFC Editor index",
|
||||
task="ietf.sync.tasks.rfc_editor_index_update_task",
|
||||
kwargs=json.dumps(dict(full_index=True)),
|
||||
defaults=dict(
|
||||
enabled=False,
|
||||
crontab=self.crontabs["daily"],
|
||||
description=(
|
||||
"Run an extended version of the rfc editor update to catch changes with backdated timestamps"
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
PeriodicTask.objects.get_or_create(
|
||||
name="Fetch meeting attendance",
|
||||
task="ietf.stats.tasks.fetch_meeting_attendance_task",
|
||||
defaults=dict(
|
||||
enabled=False,
|
||||
crontab=self.crontabs["daily"],
|
||||
description="Fetch meeting attendance data from ietf.org/registration/attendees",
|
||||
),
|
||||
)
|
||||
|
||||
PeriodicTask.objects.get_or_create(
|
||||
name="Send review reminders",
|
||||
task="ietf.review.tasks.send_review_reminders_task",
|
||||
defaults=dict(
|
||||
enabled=False,
|
||||
crontab=self.crontabs["daily"],
|
||||
description="Send reminders originating from the review app",
|
||||
),
|
||||
)
|
||||
|
||||
def show_tasks(self):
|
||||
for label, crontab in self.crontabs.items():
|
||||
tasks = PeriodicTask.objects.filter(crontab=crontab).order_by(
|
||||
"task", "name"
|
||||
)
|
||||
self.stdout.write(f"\n{label} ({crontab.human_readable})\n")
|
||||
if tasks:
|
||||
for task in tasks:
|
||||
desc = f" {task.id:-3d}: {task.task} - {task.name}"
|
||||
if task.enabled:
|
||||
self.stdout.write(desc)
|
||||
else:
|
||||
self.stdout.write(self.style.NOTICE(f"{desc} - disabled"))
|
||||
else:
|
||||
self.stdout.write(" Nothing scheduled")
|
||||
|
||||
def enable_tasks(self, pks):
|
||||
PeriodicTask.objects.filter(
|
||||
crontab__in=self.crontabs.values(), pk__in=pks
|
||||
).update(enabled=True)
|
||||
|
||||
def disable_tasks(self, pks):
|
||||
PeriodicTask.objects.filter(
|
||||
crontab__in=self.crontabs.values(), pk__in=pks
|
||||
).update(enabled=False)
|
|
@ -32,7 +32,7 @@ class Command(BaseCommand):
|
|||
person = rule.person
|
||||
if not person and not group:
|
||||
try:
|
||||
person = rule.community_list.user.person
|
||||
person = rule.community_list.person
|
||||
except:
|
||||
pass
|
||||
name = ((group and group.acronym) or (person and person.email_address())) or '?'
|
||||
|
|
|
@ -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-tastypie>=0.14.5 # Version must be locked in sync with version of Django
|
||||
django-vite>=2.0.2,<3
|
||||
django-webtest>=1.9.10 # Only used in tests
|
||||
django-widget-tweaks>=1.4.12
|
||||
djlint>=1.0.0 # To auto-indent templates via "djlint --profile django --reformat"
|
||||
docutils>=0.18.1 # Used only by dbtemplates for RestructuredText
|
||||
|
|
Loading…
Reference in a new issue