datatracker/ietf/group/tests_info.py
Robert Sparks 997239a2ea
feat: write objects to blob storage (#8557)
* feat: basic blobstore infrastructure for dev

* refactor: (broken) attempt to put minio console behind nginx

* feat: initialize blobstore with boto3

* fix: abandon attempt to proxy minio. Use docker compose instead.

* feat: beginning of blob writes

* feat: storage utilities

* feat: test buckets

* chore: black

* chore: remove unused import

* chore: avoid f string when not needed

* fix: inform all settings files about blobstores

* fix: declare types for some settings

* ci: point to new target base

* ci: adjust test workflow

* fix: give the tests debug environment a blobstore

* fix: "better" name declarations

* ci: use devblobstore container

* chore: identify places to write to blobstorage

* chore: remove unreachable code

* feat: store materials

* feat: store statements

* feat: store status changes

* feat: store liaison attachments

* feat: store agendas provided with Interim session requests

* chore: capture TODOs

* feat: store polls and chatlogs

* chore: remove unneeded TODO

* feat: store drafts on submit and post

* fix: handle storage during doc expiration and resurrection

* fix: mirror an unlink

* chore: add/refine TODOs

* feat: store slide submissions

* fix: structure slide test correctly

* fix: correct sense of existence check

* feat: store some indexes

* feat: BlobShadowFileSystemStorage

* feat: shadow floorplans / host logos to the blob

* chore: remove unused import

* feat: strip path from blob shadow names

* feat: shadow photos / thumbs

* refactor: combine photo and photothumb blob kinds

The photos / thumbs were already dropped in the same
directory, so let's not add a distinction at this point.

* style: whitespace

* refactor: use kwargs consistently

* chore: migrations

* refactor: better deconstruct(); rebuild migrations

* fix: use new class in mack patch

* chore: add TODO

* feat: store group index documents

* chore: identify more TODO

* feat: store reviews

* fix: repair merge

* chore: remove unnecessary TODO

* feat: StoredObject metadata

* fix: deburr some debugging code

* fix: only set the deleted timestamp once

* chore: correct typo

* fix: get_or_create vs get and test

* fix: avoid the questionable is_seekable helper

* chore: capture future design consideration

* chore: blob store cfg for k8s

* chore: black

* chore: copyright

* ci: bucket name prefix option + run Black

Adds/uses DATATRACKER_BLOB_STORE_BUCKET_PREFIX option. Other changes
are just Black styling.

* ci: fix typo in bucket name expression

* chore: parameters in app-configure-blobstore

Allows use with other blob stores.

* ci: remove verify=False option

* fix: don't return value from __init__

* feat: option to log timing of S3Storage calls

* chore: units

* fix: deleted->null when storing a file

* style: Black

* feat: log as JSON; refactor to share code; handle exceptions

* ci: add ietf_log_blob_timing option for k8s

* test: --no-manage-blobstore option for running tests

* test: use blob store settings from env, if set

* test: actually set a couple more storage opts

* feat: offswitch (#8541)

* feat: offswitch

* fix: apply ENABLE_BLOBSTORAGE to BlobShadowFileSystemStorage behavior

* chore: log timing of blob reads

* chore: import Config from botocore.config

* chore(deps): import boto3-stubs / botocore

botocore is implicitly imported, but make it explicit
since we refer to it directly

* chore: drop type annotation that mypy loudly ignores

* refactor: add storage methods via mixin

Shares code between Document and DocHistory without
putting it in the base DocumentInfo class, which
lacks the name field. Also makes mypy happy.

* feat: add timeout / retry limit to boto client

* ci: let k8s config the timeouts via env

* chore: repair merge resolution typo

* chore: tweak settings imports

* chore: simplify k8s/settings_local.py imports

---------

Co-authored-by: Jennifer Richards <jennifer@staff.ietf.org>
2025-02-19 17:41:10 -06:00

2268 lines
106 KiB
Python

# Copyright The IETF Trust 2009-2024, All Rights Reserved
# -*- coding: utf-8 -*-
import calendar
import datetime
import io
import bleach
import mock
from unittest.mock import call, patch
from pathlib import Path
from pyquery import PyQuery
import debug # pyflakes:ignore
from django.conf import settings
from django.http import Http404, HttpResponse
from django.test import RequestFactory
from django.test.utils import override_settings
from django.urls import reverse as urlreverse
from django.urls import NoReverseMatch
from django.utils import timezone
from django.contrib.auth.models import User
from django.utils.html import escape
from ietf.community.models import CommunityList
from ietf.community.utils import reset_name_contains_index_for_rule
from ietf.doc.factories import WgDraftFactory, IndividualDraftFactory, CharterFactory, BallotDocEventFactory
from ietf.doc.models import Document, DocEvent, State
from ietf.doc.storage_utils import retrieve_str
from ietf.doc.utils_charter import charter_name_for_group
from ietf.group.admin import GroupForm as AdminGroupForm
from ietf.group.factories import (GroupFactory, RoleFactory, GroupEventFactory,
DatedGroupMilestoneFactory, DatelessGroupMilestoneFactory)
from ietf.group.forms import GroupForm
from ietf.group.models import Group, GroupEvent, GroupMilestone, GroupStateTransitions, Role
from ietf.group.tasks import generate_wg_charters_files_task, generate_wg_summary_files_task
from ietf.group.views import response_from_file
from ietf.group.utils import save_group_in_history, setup_default_community_list_for_group
from ietf.meeting.factories import SessionFactory
from ietf.name.models import DocTagName, GroupStateName, GroupTypeName, ExtResourceName, RoleName
from ietf.person.models import Person, Email
from ietf.person.factories import PersonFactory, EmailFactory
from ietf.review.factories import ReviewRequestFactory, ReviewAssignmentFactory
from ietf.utils.mail import outbox, empty_outbox, get_payload_text
from ietf.utils.test_utils import login_testing_unauthorized, TestCase, unicontent, reload_db_objects
from ietf.utils.timezone import date_today, DEADLINE_TZINFO
def group_urlreverse_list(group, viewname):
return [
urlreverse(viewname, kwargs=dict(acronym=group.acronym)),
urlreverse(viewname, kwargs=dict(acronym=group.acronym, group_type=group.type_id)),
]
def pklist(docs):
return [ str(doc.pk) for doc in docs.all() ]
class GroupPagesTests(TestCase):
settings_temp_path_overrides = TestCase.settings_temp_path_overrides + [
"CHARTER_PATH",
"CHARTER_COPY_PATH",
"CHARTER_COPY_OTHER_PATH", # Note: not explicitly testing use of
"CHARTER_COPY_THIRD_PATH", # either of these settings
"GROUP_SUMMARY_PATH",
]
def test_active_groups(self):
area = GroupFactory.create(type_id='area')
group = GroupFactory.create(type_id='wg',parent=area)
RoleFactory(group=group,name_id='ad',person=PersonFactory())
url = urlreverse('ietf.group.views.active_groups', kwargs=dict(group_type="wg"))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertContains(r, group.parent.name)
self.assertContains(r, group.acronym)
self.assertContains(r, group.name)
self.assertContains(r, escape(group.ad_role().person.name))
for t in ('rg','area','ag', 'rag', 'dir','review','team','program','iabasg','iabworkshop','adm','rfcedtyp'): # See issue 5120
g = GroupFactory.create(type_id=t,state_id='active')
if t in ['dir','review']:
g.parent = GroupFactory.create(type_id='area',state_id='active')
g.save()
url = urlreverse('ietf.group.views.active_groups', kwargs=dict(group_type=t))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertContains(r, g.acronym)
if t == "area":
q = PyQuery(r.content)
wg_url = urlreverse("ietf.group.views.active_groups", kwargs=dict(group_type="wg"))
href = f"{wg_url}#{g.acronym.upper()}"
self.assertEqual(q(f"h2#id-{g.acronym} a").attr("href"), href)
self.assertEqual(q(f'h2#id-{g.acronym} a[href="{href}"]').text(), f"({g.acronym.upper()})")
url = urlreverse('ietf.group.views.active_groups', kwargs=dict())
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertContains(r, "Directorate")
self.assertContains(r, "AG")
for slug in GroupTypeName.objects.exclude(slug__in=['wg','rg','ag','rag','area','dir','review','team','program','adhoc','ise','adm','iabasg','iabworkshop','rfcedtyp', 'edwg', 'edappr']).values_list('slug',flat=True):
with self.assertRaises(NoReverseMatch):
url=urlreverse('ietf.group.views.active_groups', kwargs=dict(group_type=slug))
def test_group_home(self):
draft = WgDraftFactory()
group = draft.group
# TODO - move this into GroupFactory
setup_default_community_list_for_group(group)
url_list = group_urlreverse_list(group, 'ietf.group.views.group_home')
next_list = group_urlreverse_list(group, 'ietf.group.views.group_documents')
for url, next in [ (url_list[i], next_list[i]) for i in range(len(url_list)) ]:
r = self.client.get(url)
self.assertRedirects(r, next)
r = self.client.get(next)
self.assertContains(r, group.acronym)
self.assertContains(r, group.name)
for word in ['Documents', 'Date', 'Status', 'IPR', 'AD', 'Shepherd']:
self.assertContains(r, word)
self.assertContains(r, draft.name)
self.assertContains(r, draft.title)
def test_response_from_file(self):
# n.b., GROUP_SUMMARY_PATH is a temp dir that will be cleaned up automatically
fp = Path(settings.GROUP_SUMMARY_PATH) / "some-file.txt"
fp.write_text("This is a charters file with an é")
r = response_from_file(fp)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.headers["Content-Type"], "text/plain; charset=utf-8")
self.assertEqual(r.content.decode("utf8"), "This is a charters file with an é")
# now try with a nonexistent file
fp.unlink()
with self.assertRaises(Http404):
response_from_file(fp)
@patch("ietf.group.views.response_from_file")
def test_wg_summary_area(self, mock):
r = self.client.get(
urlreverse("ietf.group.views.wg_summary_area", kwargs={"group_type": "rg"})
) # not wg
self.assertEqual(r.status_code, 404)
self.assertFalse(mock.called)
mock.return_value = HttpResponse("yay")
r = self.client.get(
urlreverse("ietf.group.views.wg_summary_area", kwargs={"group_type": "wg"})
)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.content.decode(), "yay")
self.assertEqual(mock.call_args, call(Path(settings.GROUP_SUMMARY_PATH) / "1wg-summary.txt"))
@patch("ietf.group.views.response_from_file")
def test_wg_summary_acronym(self, mock):
r = self.client.get(
urlreverse(
"ietf.group.views.wg_summary_acronym", kwargs={"group_type": "rg"}
)
) # not wg
self.assertEqual(r.status_code, 404)
self.assertFalse(mock.called)
mock.return_value = HttpResponse("yay")
r = self.client.get(
urlreverse(
"ietf.group.views.wg_summary_acronym", kwargs={"group_type": "wg"}
)
)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.content.decode(), "yay")
self.assertEqual(
mock.call_args, call(Path(settings.GROUP_SUMMARY_PATH) / "1wg-summary-by-acronym.txt")
)
@patch("ietf.group.views.response_from_file")
def test_wg_charters(self, mock):
r = self.client.get(
urlreverse("ietf.group.views.wg_charters", kwargs={"group_type": "rg"})
) # not wg
self.assertEqual(r.status_code, 404)
self.assertFalse(mock.called)
mock.return_value = HttpResponse("yay")
r = self.client.get(
urlreverse("ietf.group.views.wg_charters", kwargs={"group_type": "wg"})
)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.content.decode(), "yay")
self.assertEqual(mock.call_args, call(Path(settings.CHARTER_PATH) / "1wg-charters.txt"))
@patch("ietf.group.views.response_from_file")
def test_wg_charters_by_acronym(self, mock):
r = self.client.get(
urlreverse(
"ietf.group.views.wg_charters_by_acronym", kwargs={"group_type": "rg"}
)
) # not wg
self.assertEqual(r.status_code, 404)
self.assertFalse(mock.called)
mock.return_value = HttpResponse("yay")
r = self.client.get(
urlreverse(
"ietf.group.views.wg_charters_by_acronym", kwargs={"group_type": "wg"}
)
)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.content.decode(), "yay")
self.assertEqual(
mock.call_args, call(Path(settings.CHARTER_PATH) / "1wg-charters-by-acronym.txt")
)
def test_generate_wg_charters_files_task(self):
group = CharterFactory(
group__type_id="wg", group__parent=GroupFactory(type_id="area")
).group
RoleFactory(group=group, name_id="chair", person=PersonFactory())
RoleFactory(group=group, name_id="ad", person=PersonFactory())
chair = Email.objects.filter(role__group=group, role__name="chair")[0]
(
Path(settings.CHARTER_PATH) / f"{group.charter.name}-{group.charter.rev}.txt"
).write_text("This is a charter.")
generate_wg_charters_files_task()
wg_charters_contents = (Path(settings.CHARTER_PATH) / "1wg-charters.txt").read_text(
encoding="utf8"
)
self.assertIn(group.acronym, wg_charters_contents)
self.assertIn(group.name, wg_charters_contents)
self.assertIn(group.ad_role().person.plain_name(), wg_charters_contents)
self.assertIn(chair.address, wg_charters_contents)
self.assertIn("This is a charter.", wg_charters_contents)
wg_charters_copy = (
Path(settings.CHARTER_COPY_PATH) / "1wg-charters.txt"
).read_text(encoding="utf8")
self.assertEqual(wg_charters_copy, wg_charters_contents)
wg_charters_by_acronym_contents = (
Path(settings.CHARTER_PATH) / "1wg-charters-by-acronym.txt"
).read_text(encoding="utf8")
self.assertIn(group.acronym, wg_charters_by_acronym_contents)
self.assertIn(group.name, wg_charters_by_acronym_contents)
self.assertIn(group.ad_role().person.plain_name(), wg_charters_by_acronym_contents)
self.assertIn(chair.address, wg_charters_by_acronym_contents)
self.assertIn("This is a charter.", wg_charters_by_acronym_contents)
wg_charters_by_acronymcopy = (
Path(settings.CHARTER_COPY_PATH) / "1wg-charters-by-acronym.txt"
).read_text(encoding="utf8")
self.assertEqual(wg_charters_by_acronymcopy, wg_charters_by_acronym_contents)
def test_generate_wg_charters_files_task_without_copy(self):
"""Test disabling charter file copying
Note that these tests mostly check that errors are not encountered. Because they unset
the CHARTER_COPY_PATH or set it to a non-directory destination, it's not clear where to
look to see whether the files were (incorrectly) copied somewhere.
"""
group = CharterFactory(
group__type_id="wg", group__parent=GroupFactory(type_id="area")
).group
(
Path(settings.CHARTER_PATH) / f"{group.charter.name}-{group.charter.rev}.txt"
).write_text("This is a charter.")
# No directory set
with override_settings():
del settings.CHARTER_COPY_PATH
generate_wg_charters_files_task()
# n.b., CHARTER_COPY_PATH is set again outside the with block
self.assertTrue((Path(settings.CHARTER_PATH) / "1wg-charters.txt").exists())
self.assertFalse((Path(settings.CHARTER_COPY_PATH) / "1wg-charters.txt").exists())
self.assertTrue(
(Path(settings.CHARTER_PATH) / "1wg-charters-by-acronym.txt").exists()
)
self.assertFalse(
(Path(settings.CHARTER_COPY_PATH) / "1wg-charters-by-acronym.txt").exists()
)
(Path(settings.CHARTER_PATH) / "1wg-charters.txt").unlink()
(Path(settings.CHARTER_PATH) / "1wg-charters-by-acronym.txt").unlink()
# Set to a file, not a directory
not_a_dir = Path(settings.CHARTER_COPY_PATH) / "not-a-dir.txt"
not_a_dir.write_text("Not a dir")
with override_settings(CHARTER_COPY_PATH=str(not_a_dir)):
generate_wg_charters_files_task()
# n.b., CHARTER_COPY_PATH is set again outside the with block
self.assertTrue((Path(settings.CHARTER_PATH) / "1wg-charters.txt").exists())
self.assertFalse((Path(settings.CHARTER_COPY_PATH) / "1wg-charters.txt").exists())
self.assertTrue(
(Path(settings.CHARTER_PATH) / "1wg-charters-by-acronym.txt").exists()
)
self.assertFalse(
(Path(settings.CHARTER_COPY_PATH) / "1wg-charters-by-acronym.txt").exists()
)
self.assertEqual(not_a_dir.read_text(), "Not a dir")
def test_generate_wg_summary_files_task(self):
group = CharterFactory(group__type_id='wg',group__parent=GroupFactory(type_id='area')).group
RoleFactory(group=group,name_id='chair',person=PersonFactory())
RoleFactory(group=group,name_id='ad',person=PersonFactory())
chair = Email.objects.filter(role__group=group, role__name="chair")[0]
generate_wg_summary_files_task()
for summary_by_area_contents in [
(
Path(settings.GROUP_SUMMARY_PATH) / "1wg-summary.txt"
).read_text(encoding="utf8"),
retrieve_str("indexes", "1wg-summary.txt")
]:
self.assertIn(group.parent.name, summary_by_area_contents)
self.assertIn(group.acronym, summary_by_area_contents)
self.assertIn(group.name, summary_by_area_contents)
self.assertIn(chair.address, summary_by_area_contents)
for summary_by_acronym_contents in [
(
Path(settings.GROUP_SUMMARY_PATH) / "1wg-summary-by-acronym.txt"
).read_text(encoding="utf8"),
retrieve_str("indexes", "1wg-summary-by-acronym.txt")
]:
self.assertIn(group.acronym, summary_by_acronym_contents)
self.assertIn(group.name, summary_by_acronym_contents)
self.assertIn(chair.address, summary_by_acronym_contents)
def test_chartering_groups(self):
group = CharterFactory(group__type_id='wg',group__parent=GroupFactory(type_id='area'),states=[('charter','intrev')]).group
url = urlreverse('ietf.group.views.chartering_groups')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('#content a:contains("%s")' % group.acronym)), 1)
self.client.login(username="secretary", password="secretary+password")
r = self.client.get(url)
self.assertContains(r, "Charter new RG")
self.assertContains(r, "Charter new WG")
self.client.login(username="ad", password="ad+password")
r = self.client.get(url)
self.assertNotContains(r, "Charter new RG")
self.assertContains(r, "Charter new WG")
self.client.login(username="irtf-chair", password="irtf-chair+password")
r = self.client.get(url)
self.assertContains(r, "Charter new RG")
self.assertNotContains(r, "Charter new WG")
self.client.logout()
replaced_group = CharterFactory(group__state_id='replaced',group__type_id='wg',group__parent=GroupFactory(type_id='area'),states=[('charter','intrev')]).group
r = self.client.get(url)
q = PyQuery(r.content)
self.assertEqual(len(q('#content a:contains("%s")' % replaced_group.acronym)), 0)
def test_concluded_groups(self):
group = GroupFactory(state_id='conclude')
url = urlreverse('ietf.group.views.concluded_groups')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('#content a:contains("%s")' % group.acronym)), 1)
def test_bofs(self):
group = GroupFactory(state_id='bof')
url = urlreverse('ietf.group.views.bofs', kwargs=dict(group_type="wg"))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('#content a:contains("%s")' % group.acronym)), 1)
def test_group_documents(self):
group = GroupFactory()
setup_default_community_list_for_group(group)
draft = WgDraftFactory(group=group)
draft.action_holders.set([PersonFactory()])
draft2 = WgDraftFactory(group=group)
draft3 = WgDraftFactory(group=group)
draft3.set_state(State.objects.get(type='draft-iesg', slug='pub-req'))
draft3.action_holders.set(PersonFactory.create_batch(2))
old_dah = draft3.documentactionholder_set.first()
old_dah.time_added -= datetime.timedelta(days=173) # make an "old" action holder
old_dah.save()
draft4 = WgDraftFactory(group=group)
draft4.set_state(State.objects.get(type='draft', slug='expired')) # Expired WG draft
draft5 = IndividualDraftFactory()
draft5.set_state(State.objects.get(type='draft', slug='expired')) # Expired non-WG draft
draft6 = WgDraftFactory(group=group)
draft6.set_state(State.objects.get(type='draft', slug='expired'))
draft6.set_state(State.objects.get(type='draft-iesg', slug='dead')) # Expired WG draft, marked as dead
draft7 = WgDraftFactory(group=group)
draft7.set_state(State.objects.get(type='draft', slug='expired'))
draft7.set_state(State.objects.get(type='draft-stream-%s' % draft7.stream_id, slug='dead')) # Expired WG draft, marked as dead
clist = CommunityList.objects.get(group=group)
related_docs_rule = clist.searchrule_set.get(rule_type='name_contains')
reset_name_contains_index_for_rule(related_docs_rule)
for url in group_urlreverse_list(group, 'ietf.group.views.group_documents'):
with self.settings(DOC_ACTION_HOLDER_MAX_AGE_DAYS=20):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertContains(r, draft.name)
self.assertContains(r, group.name)
self.assertContains(r, group.acronym)
self.assertNotContains(r, draft.action_holders.first().plain_name())
self.assertContains(r, draft2.name)
self.assertContains(r, draft3.name)
for ah in draft3.action_holders.all():
self.assertContains(r, escape(ah.name))
self.assertContains(r, 'for 173 days', count=1) # the old_dah should be tagged
self.assertContains(r, draft4.name)
self.assertNotContains(r, draft5.name)
self.assertNotContains(r, draft6.name)
self.assertNotContains(r, draft7.name)
# Make sure that a logged in user is presented with an opportunity to add results to their community list
self.client.login(username="secretary", password="secretary+password")
r = self.client.get(url)
q = PyQuery(r.content)
self.assertTrue(any([draft2.name in x.attrib['href'] for x in q('table td a.track-untrack-doc')]))
# test the txt version too while we're at it
for url in group_urlreverse_list(group, 'ietf.group.views.group_documents_txt'):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertContains(r, draft.name)
self.assertContains(r, draft2.name)
def test_group_charter(self):
group = CharterFactory().group
draft = WgDraftFactory(group=group)
(
Path(settings.CHARTER_PATH) / f"{group.charter.name}-{group.charter.rev}.txt"
).write_text("This is a charter.")
milestone = GroupMilestone.objects.create(
group=group,
state_id="active",
desc="Get Work Done",
due=date_today(DEADLINE_TZINFO) + datetime.timedelta(days=100))
milestone.docs.add(draft)
for url in [group.about_url(),] + group_urlreverse_list(group, 'ietf.group.views.group_about'):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertContains(r, group.name)
self.assertContains(r, group.acronym)
self.assertContains(r, "This is a charter.")
self.assertContains(r, milestone.desc)
self.assertContains(r, milestone.docs.all()[0].name)
def test_group_about(self):
interesting_users = [ 'plain','iana','iab-chair','irtf-chair', 'marschairman', 'teamchairman','ad', 'iab-member', 'secretary', ]
can_edit = {
'wg' : ['secretary','ad'],
'rg' : ['secretary','irtf-chair'],
'ag' : ['secretary', 'ad' ],
'rag' : ['secretary', 'irtf-chair'],
'team' : ['secretary' , 'ad'],
'dir' : ['secretary',],
'review' : ['secretary',],
'program' : ['secretary', 'iab-member'],
}
def setup_role(group, role_id):
p = PersonFactory(user__username="%s_%s"%(group.acronym,role_id))
group.role_set.create(name_id=role_id,person=p,email=p.email())
can_edit[group.type_id].append(p.user.username)
interesting_users.append(p.user.username)
test_groups = []
for t in ['wg','rg','ag','rag','team']:
g = GroupFactory(type_id=t)
setup_role(g,'chair')
test_groups.append(g)
for t in ['dir','review',]:
g = GroupFactory(type_id=t)
setup_role(g,'secr')
test_groups.append(g)
g = GroupFactory(type_id='program')
setup_role(g, 'lead')
test_groups.append(g)
def verify_cannot_edit_group(url, group, username):
self.client.logout()
self.client.login(username=username, password=username+"+password")
r = self.client.get(url)
self.assertTrue(r.status_code in (302,403),"%s should not be able to edit %s of type %s"%(username,group.acronym,group.type_id))
def verify_can_edit_group(url, group, username):
self.client.logout()
self.client.login(username=username, password=username+"+password")
r = self.client.get(url)
self.assertEqual(r.status_code, 200, "%s should be able to edit %s of type %s"%(username,group.acronym,group.type_id))
for group in test_groups:
for url in [group.about_url(),] + group_urlreverse_list(group, 'ietf.group.views.group_about'):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertContains(r, group.name)
self.assertContains(r, group.acronym)
self.assertContains(r, group.description)
for url in group_urlreverse_list(group, 'ietf.group.views.edit'):
for username in can_edit[group.type_id]:
verify_can_edit_group(url, group, username)
for username in list(set(interesting_users)-set(can_edit[group.type_id])):
verify_cannot_edit_group(url, group, username)
def test_group_about_personnel(self):
"""Correct personnel should appear on the group About page"""
group = GroupFactory()
for role_name in group.features.default_used_roles:
RoleFactory.create_batch(2, group=group, name=RoleName.objects.get(slug=role_name))
for url in [group.about_url(),] + group_urlreverse_list(group, 'ietf.group.views.group_about'):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
for role in group.role_set.all():
self.assertContains(r, escape(role.person.name))
def test_group_about_nosubscribe(self):
group = GroupFactory()
self.assertEqual(group.list_subscribe, '')
url = urlreverse('ietf.group.views.group_about', kwargs=dict(acronym=group.acronym))
r = self.client.get(url)
self.assertNotContains(r,'To subscribe')
group.list_subscribe='foo@example.com'
group.save()
r = self.client.get(url)
self.assertContains(r,'To subscribe')
def test_materials(self):
group = GroupFactory(type_id="team", acronym="testteam", name="Test Team", state_id="active")
doc = Document.objects.create(
name="slides-testteam-test-slides",
rev="00",
title="Test Slides",
group=group,
type_id="slides",
)
doc.set_state(State.objects.get(type="slides", slug="active"))
for url in group_urlreverse_list(group, 'ietf.group.views.materials'):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertContains(r, doc.title)
self.assertContains(r, doc.name)
url = urlreverse("ietf.group.views.materials", kwargs={ 'acronym': group.acronym })
# try deleting the document and check it's gone
doc.set_state(State.objects.get(type="slides", slug="deleted"))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertNotContains(r, doc.title)
def test_history(self):
group = GroupFactory()
e = GroupEvent.objects.create(
group=group,
desc="Something happened.",
type="added_comment",
by=Person.objects.get(name="(System)"))
for url in group_urlreverse_list(group, 'ietf.group.views.history'):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertContains(r, e.desc)
def test_feed(self):
group = CharterFactory().group
ge = GroupEvent.objects.create(
group=group,
desc="Something happened.",
type="added_comment",
by=Person.objects.get(name="(System)"))
de = DocEvent.objects.create(
doc=group.charter,
rev=group.charter.rev,
desc="Something else happened.",
type="added_comment",
by=Person.objects.get(name="(System)"))
r = self.client.get("/feed/group-changes/%s/" % group.acronym)
self.assertEqual(r.status_code, 200)
self.assertContains(r, ge.desc)
self.assertContains(r, de.desc)
def test_chair_photos(self):
RoleFactory(name_id='chair')
url = urlreverse("ietf.group.views.chair_photos", kwargs={'group_type':'wg'})
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
chairs = Role.objects.filter(group__type='wg', group__state='active', name_id='chair')
self.assertEqual(len(q('.photo')), chairs.count())
def test_wg_photos(self):
GroupFactory(acronym='mars')
RoleFactory(name_id='chair')
RoleFactory(name_id='secr')
url = urlreverse("ietf.group.views.group_photos", kwargs={'group_type':'wg', 'acronym':'mars'})
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
roles = Role.objects.filter(group__acronym='mars')
self.assertEqual(len(q('.photo')), roles.count())
def test_group_photos(self):
url = urlreverse("ietf.group.views.group_photos", kwargs={'acronym':'iab'})
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
roles = Role.objects.filter(group__acronym='iab')
self.assertEqual(len(q('.photo')), roles.count())
def test_nonactive_group_badges(self):
concluded_group = GroupFactory(state_id='conclude')
url = urlreverse("ietf.group.views.history",kwargs={'acronym':concluded_group.acronym})
r = self.client.get(url)
self.assertEqual(r.status_code,200)
q = PyQuery(r.content)
self.assertEqual(q('.badge.text-bg-warning').text(),"Concluded WG")
replaced_group = GroupFactory(state_id='replaced')
url = urlreverse("ietf.group.views.history",kwargs={'acronym':replaced_group.acronym})
r = self.client.get(url)
self.assertEqual(r.status_code,200)
q = PyQuery(r.content)
self.assertEqual(q('.badge.text-bg-warning').text(),"Replaced WG")
class GroupEditTests(TestCase):
settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ['CHARTER_PATH']
def test_create(self):
url = urlreverse('ietf.group.views.edit', kwargs=dict(group_type="wg", action="charter"))
login_testing_unauthorized(self, "secretary", url)
num_wgs = len(Group.objects.filter(type="wg"))
bof_state = GroupStateName.objects.get(slug="bof")
area = Group.objects.filter(type="area").first()
# normal get
for username in ("secretary","ad","irtf-chair"):
self.client.logout()
login_testing_unauthorized(self, username, url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('form input[name=acronym]')), 1)
self.client.logout()
login_testing_unauthorized(self, "secretary", url)
# faulty post
r = self.client.post(url, dict(acronym="foobarbaz")) # No name
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form .is-invalid')) > 0)
self.assertEqual(len(Group.objects.filter(type="wg")), num_wgs)
# acronym contains non-alphanumeric
r = self.client.post(url, dict(acronym="test...", name="Testing WG", state=bof_state.pk))
self.assertEqual(r.status_code, 200)
self.assertTrue(len(q('form .is-invalid')) > 0)
# acronym contains hyphen
r = self.client.post(url, dict(acronym="test-wg", name="Testing WG", state=bof_state.pk))
self.assertEqual(r.status_code, 200)
self.assertTrue(len(q('form .is-invalid')) > 0)
# acronym too short
r = self.client.post(url, dict(acronym="t", name="Testing WG", state=bof_state.pk))
self.assertEqual(r.status_code, 200)
self.assertTrue(len(q('form .is-invalid')) > 0)
# acronym doesn't start with an alpha character
r = self.client.post(url, dict(acronym="1startwithalpha", name="Testing WG", state=bof_state.pk))
self.assertEqual(r.status_code, 200)
self.assertTrue(len(q('form .is-invalid')) > 0)
# no parent group given
r = self.client.post(url, dict(acronym="testwg", name="Testing WG", state=bof_state.pk))
self.assertEqual(r.status_code, 200)
self.assertTrue(len(q('form .is-invalid')) > 0)
# Ok creation
r = self.client.post(
url,
dict(acronym="testwg", name="Testing WG", state=bof_state.pk, parent=area.pk, description="ignored"),
)
self.assertEqual(r.status_code, 302)
self.assertEqual(len(Group.objects.filter(type="wg")), num_wgs + 1)
group = Group.objects.get(acronym="testwg")
self.assertEqual(group.name, "Testing WG")
self.assertEqual(charter_name_for_group(group), "charter-ietf-testwg")
self.assertEqual(group.description, '', 'Description should be ignored for a WG')
def test_create_rg(self):
url = urlreverse('ietf.group.views.edit', kwargs=dict(group_type="rg", action="charter"))
irtf = Group.objects.get(acronym='irtf')
num_rgs = len(Group.objects.filter(type="rg"))
proposed_state = GroupStateName.objects.get(slug="proposed")
# normal get
for username in ("secretary", "ad", "irtf-chair"):
self.client.logout()
login_testing_unauthorized(self, username, url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('form input[name=acronym]')), 1)
self.assertEqual(q('form select[name=parent]')[0].value,'%s'%irtf.pk)
r = self.client.post(url, dict(acronym="testrg", name="Testing RG", state=proposed_state.pk, parent=irtf.pk))
self.assertEqual(r.status_code, 302)
self.assertEqual(len(Group.objects.filter(type="rg")), num_rgs + 1)
group = Group.objects.get(acronym="testrg")
self.assertEqual(group.name, "Testing RG")
self.assertEqual(charter_name_for_group(group), "charter-irtf-testrg")
def test_create_based_on_existing_bof(self):
url = urlreverse('ietf.group.views.edit', kwargs=dict(group_type="wg", action="charter"))
login_testing_unauthorized(self, "secretary", url)
group = GroupFactory(acronym="mars",parent=GroupFactory(type_id='area'))
# try hijacking area - faulty
r = self.client.post(url, dict(name="Test", acronym=group.parent.acronym))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form .is-invalid')) > 0)
# try elevating BOF to WG
group.state_id = "bof"
group.save()
r = self.client.post(url, dict(name="Test", acronym=group.acronym))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form .is-invalid')) > 0)
self.assertEqual(Group.objects.get(acronym=group.acronym).state_id, "bof")
# # confirm elevation
# state = GroupStateName.objects.get(slug="proposed")
# r = self.client.post(url, dict(name="Test", acronym=group.acronym, confirm_acronym="1", state=state.pk))
# self.assertEqual(r.status_code, 302)
# self.assertEqual(Group.objects.get(acronym=group.acronym).state_id, "proposed")
# self.assertEqual(Group.objects.get(acronym=group.acronym).name, "Test")
def test_create_non_chartered_includes_description(self):
parent = GroupFactory(type_id='area')
group_type = GroupTypeName.objects.filter(used=True, features__has_chartering_process=False, features__parent_types='area').first()
self.assertIsNotNone(group_type)
url = urlreverse('ietf.group.views.edit', kwargs=dict(group_type=group_type.slug, action="create"))
login_testing_unauthorized(self, "secretary", url)
r = self.client.post(
url,
{
'acronym': "testgrp",
'name': "Testing",
'state': GroupStateName.objects.get(slug='active').pk,
'parent': parent.pk,
'description': "not ignored",
},
)
self.assertEqual(r.status_code, 302)
group = Group.objects.get(acronym="testgrp")
self.assertEqual(group.name, "Testing")
self.assertEqual(group.description, 'not ignored', 'Description should not be ignored')
def test_edit_info(self):
group = GroupFactory(acronym='mars',parent=GroupFactory(type_id='area'))
CharterFactory(group=group)
RoleFactory(group=group,name_id='chair',person__user__email='marschairman@example.org')
RoleFactory(group=group,name_id='delegate',person__user__email='marsdelegate@example.org')
url = urlreverse('ietf.group.views.edit', kwargs=dict(group_type=group.type_id, acronym=group.acronym, action="edit"))
login_testing_unauthorized(self, "secretary", url)
# normal get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('form select[name=parent]')), 1)
self.assertEqual(len(q('form input[name=acronym]')), 1)
for role_slug in group.used_roles or group.features.default_used_roles:
self.assertEqual(len(q('form select[name=%s_roles]'%role_slug)),1)
# faulty post
Group.objects.create(name="Collision Test Group", acronym="collide")
r = self.client.post(url, dict(acronym="collide"))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form .is-invalid')) > 0)
# create old acronym
group.acronym = "oldmars"
group.save()
save_group_in_history(group)
group.acronym = "mars"
group.save()
# post with warning
r = self.client.post(url, dict(acronym="oldmars"))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form .is-invalid')) > 0)
# edit info
(
Path(settings.CHARTER_PATH) / f"{group.charter.name}-{group.charter.rev}.txt"
).write_text("This is a charter.")
area = group.parent
ad = Person.objects.get(name="Areað Irector")
state = GroupStateName.objects.get(slug="bof")
empty_outbox()
r = self.client.post(url,
dict(name="Mars Not Special Interest Group",
acronym="mars",
parent=area.pk,
ad=ad.pk,
state=state.pk,
ad_roles=ad.email().address,
chair_roles=["aread@example.org", "ad1@example.org"],
secr_roles=["aread@example.org", "ad1@example.org", "ad2@example.org"],
liaison_contact_roles=["ad1@example.org"],
liaison_cc_contact_roles=["aread@example.org", "ad2@example.org"],
techadv_roles=["aread@example.org"],
delegate_roles=["ad2@example.org"],
list_email="mars@mail",
list_subscribe="subscribe.mars",
list_archive="archive.mars",
description='ignored'
))
self.assertEqual(r.status_code, 302)
group = Group.objects.get(acronym="mars")
self.assertEqual(group.name, "Mars Not Special Interest Group")
self.assertEqual(group.parent, area)
self.assertEqual(group.ad_role().person, ad)
for k in ("chair", "secr", "techadv", "liaison_cc_contact"):
self.assertTrue(group.role_set.filter(name=k, email__address="aread@example.org"))
self.assertTrue(group.role_set.filter(name='liaison_contact', email__address='ad1@example.org'))
self.assertFalse(group.role_set.filter(name='liaison_contact', email__address='aread@example.org'))
self.assertFalse(group.role_set.filter(name='liaison_contact', email__address='ad2@example.org'))
self.assertFalse(group.role_set.filter(name='liaison_cc_contact', email__address='ad1@example.org'))
self.assertTrue(group.role_set.filter(name='liaison_cc_contact', email__address='ad2@example.org'))
self.assertTrue(group.role_set.filter(name="delegate", email__address="ad2@example.org"))
self.assertEqual(group.list_email, "mars@mail")
self.assertEqual(group.list_subscribe, "subscribe.mars")
self.assertEqual(group.list_archive, "archive.mars")
self.assertEqual(group.description, '')
self.assertTrue(
(Path(settings.CHARTER_PATH) / f"{group.charter.name}-{group.charter.rev}.txt").exists()
)
self.assertEqual(len(outbox), 2)
self.assertTrue('Personnel change' in outbox[0]['Subject'])
for prefix in ['ad1','ad2','aread','marschairman','marsdelegate']:
self.assertTrue(prefix+'@' in outbox[0]['To'])
self.assertTrue(get_payload_text(outbox[0]).startswith('Sec Retary'))
def test_edit_extresources(self):
group = GroupFactory(acronym='mars',parent=GroupFactory(type_id='area'))
CharterFactory(group=group)
ExtResourceName.objects.create(slug='keymaster', name='Keymaster', type_id='email')
url = urlreverse('ietf.group.views.edit', kwargs=dict(group_type=group.type_id, acronym=group.acronym, action="edit", field="resources"))
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
self.assertEqual(r.status_code,200)
q = PyQuery(r.content)
self.assertEqual(len(q('form textarea[id=id_resources]')),1)
badlines = (
'github_repo https://github3.com/some/repo',
'github_notify badaddr',
'website /not/a/good/url',
'notavalidtag blahblahblah',
'github_repo',
)
for line in badlines:
r = self.client.post(url, dict(resources=line, submit="1"))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(q('.is-invalid'))
goodlines = """
github_repo https://github.com/some/repo Some display text
github_username githubuser
webpage http://example.com/http/is/fine
jabber_room xmpp:mars@jabber.example.com
keymaster keymaster@example.org Group Rooter
"""
r = self.client.post(url, dict(resources=goodlines, submit="1"))
self.assertEqual(r.status_code,302)
group = Group.objects.get(acronym=group.acronym)
self.assertEqual(group.latest_event(GroupEvent,type="info_changed").desc[:20], 'Resources changed to')
self.assertIn('github_username githubuser', group.latest_event(GroupEvent,type="info_changed").desc)
self.assertEqual(group.groupextresource_set.count(), 5)
self.assertEqual(group.groupextresource_set.get(name__slug='github_repo').display_name, 'Some display text')
self.assertIn(group.groupextresource_set.first().name.slug, str(group.groupextresource_set.first()))
# exercise format_resources
r = self.client.get(url)
self.assertIn('Group Rooter', unicontent(r))
def test_edit_field(self):
def _test_field(group, field_name, field_content, prohibited_form_names):
url = urlreverse('ietf.group.views.edit',
kwargs=dict(
acronym=group.acronym,
action="edit",
field=field_name
))
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('div#content > form input[name=%s], div#content > form select[name=%s]' % (field_name, field_name))), 1)
for prohibited_name in prohibited_form_names:
self.assertEqual(len(q('div#content > form input[name=%s], div#content > form select[name=%s]' % (prohibited_name, prohibited_name))), 0)
# edit info
r = self.client.post(url, {field_name: field_content})
self.assertEqual(r.status_code, 302)
#
group = Group.objects.get(acronym=group.acronym)
if field_name.endswith('_roles'):
role_name = field_name[:-len('_roles')]
self.assertSetEqual(
{fc.strip() for fc in field_content},
set(group.role_set.filter(name=role_name).values_list('email', flat=True))
)
else:
self.assertEqual(getattr(group, field_name), field_content)
self.client.logout()
group = GroupFactory(acronym="mars")
EmailFactory(address='user@example.com')
EmailFactory(address='other_user@example.com')
# Test various fields
_test_field(group, 'name', 'Mars Not Special Interest Group', ['acronym'])
_test_field(group, 'list_email', 'mars@mail', ['name'])
_test_field(group, 'liaison_contact_roles', ['user@example.com', 'other_user@example.com'], ['list_email'])
_test_field(group, 'liaison_cc_contact_roles', ['user@example.com', 'other_user@example.com'], ['liaison_contact'])
def test_edit_reviewers(self):
group=GroupFactory(type_id='review',parent=GroupFactory(type_id='area'))
other_group=GroupFactory(type_id='review',parent=GroupFactory(type_id='area'))
review_req = ReviewRequestFactory(team=group)
other_review_req = ReviewRequestFactory(team=other_group)
# Set up a reviewer that has two email addresses
reviewer = PersonFactory()
EmailFactory(person=reviewer)
first_email = reviewer.email_set.first()
last_email = reviewer.email_set.last()
RoleFactory(group=other_group, name_id='reviewer', person=reviewer, email=first_email)
url = urlreverse('ietf.group.views.edit', kwargs=dict(group_type=group.type_id, acronym=group.acronym, action='edit'))
login_testing_unauthorized(self, 'secretary', url)
# normal get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('form select[name=reviewer_roles]')), 1)
# set reviewers
empty_outbox()
post_data = dict(
name=group.name,
acronym=group.acronym,
parent=group.parent_id,
ad=Person.objects.get(name='Areað Irector').pk,
state=group.state_id,
list_email=group.list_email,
list_subscribe=group.list_subscribe,
list_archive=group.list_archive,
urls=''
)
r = self.client.post(url, dict(post_data, reviewer_roles=first_email.address))
self.assertEqual(r.status_code, 302)
self.assertEqual(group.role_set.get(name='reviewer').email.address, first_email.address)
self.assertTrue('Personnel change' in outbox[0]['Subject'])
# Assign reviews to the reviewer, then remove the reviewer from the group
# As the request deadline has not passed, the assignment should be set to withdrawn
# Reviews assigned to other groups must not be affected
review_assignment = ReviewAssignmentFactory(review_request=review_req, state_id='assigned', reviewer=first_email)
other_review_assignment = ReviewAssignmentFactory(review_request=other_review_req, state_id='assigned', reviewer=first_email)
r = self.client.post(url, post_data)
self.assertEqual(r.status_code, 302)
self.assertFalse(group.role_set.filter(name='reviewer').exists())
self.assertEqual(other_group.role_set.get(name='reviewer').email.address, first_email.address)
review_assignment, other_review_assignment = reload_db_objects(review_assignment, other_review_assignment)
self.assertEqual(review_assignment.state_id, 'withdrawn')
self.assertEqual(other_review_assignment.state_id, 'assigned')
# Repeat after adding reviewer again, but now beyond request deadline
r = self.client.post(url, dict(post_data, reviewer_roles=first_email.address))
self.assertEqual(r.status_code, 302)
review_assignment.state_id = 'accepted'
review_assignment.save()
review_req.deadline = date_today(DEADLINE_TZINFO) - datetime.timedelta(days=1)
review_req.save()
r = self.client.post(url, post_data)
self.assertEqual(r.status_code, 302)
review_assignment, other_review_assignment = reload_db_objects(review_assignment, other_review_assignment)
self.assertEqual(review_assignment.state_id, 'no-response')
self.assertEqual(other_review_assignment.state_id, 'assigned')
# Configure group with two reviewer Roles for the same person with different email addresses
# then remove one of the roles. The result should be no change to the review assignments
group.role_set.filter(name_id='reviewer').delete()
for email in reviewer.email_set.all():
group.role_set.create(name_id='reviewer', person=reviewer, email=email)
review_assignment.state_id = 'accepted'
review_assignment.save()
r = self.client.post(url, dict(post_data, reviewer_roles=last_email.address))
self.assertEqual(group.role_set.get(name='reviewer').email.address, last_email.address)
review_assignment, other_review_assignment = reload_db_objects(review_assignment, other_review_assignment)
self.assertEqual(review_assignment.state_id, 'accepted')
self.assertEqual(other_review_assignment.state_id, 'assigned')
def test_edit_info_non_chartered_includes_description(self):
group_type = GroupTypeName.objects.filter(used=True, features__has_chartering_process=False).first()
self.assertIsNotNone(group_type)
group = GroupFactory(type_id=group_type.pk, description='Original description')
url = urlreverse('ietf.group.views.edit', kwargs={'acronym': group.acronym, 'action': 'edit'})
PersonFactory(user__username='plain')
self.client.login(username='plain', password='plain+password')
# mock the auth check so we don't have to delve into details of GroupFeatures for testing
with patch('ietf.group.views.can_manage_group', return_value=True):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(q('textarea[name="description"]'))
with patch('ietf.group.views.can_manage_group', return_value=True):
r = self.client.post(url, {
'name': group.name,
'acronym': group.acronym,
'state': group.state.pk,
'description': 'Updated description',
})
self.assertEqual(r.status_code, 302)
group = Group.objects.get(pk=group.pk) # refresh
self.assertEqual(group.description, 'Updated description')
def test_edit_description_field(self):
group_type = GroupTypeName.objects.filter(used=True, features__has_chartering_process=False).first()
self.assertIsNotNone(group_type)
group = GroupFactory(type_id=group_type.pk, description='Original description')
url = urlreverse('ietf.group.views.edit',
kwargs={'acronym': group.acronym, 'action': 'edit', 'field': 'description'})
PersonFactory(user__username='plain')
self.client.login(username='plain', password='plain+password')
# mock the auth check so we don't have to delve into details of GroupFeatures for testing
with patch('ietf.group.views.can_manage_group', return_value=True):
r = self.client.post(url, {
'description': 'Updated description',
})
self.assertEqual(r.status_code, 302)
group = Group.objects.get(pk=group.pk) # refresh
self.assertEqual(group.description, 'Updated description')
# Convert the group to a chartered type and repeat - should no longer be able to edit the desc
group.type = GroupTypeName.objects.filter(used=True, features__has_chartering_process=True).first()
group.save()
with patch('ietf.group.views.can_manage_group', return_value=True):
r = self.client.post(url, {
'description': 'Ignored description',
})
self.assertEqual(r.status_code, 403)
group = Group.objects.get(pk=group.pk) # refresh
self.assertEqual(group.description, 'Updated description')
def test_edit_parent(self):
group = GroupFactory.create(type_id='wg', parent=GroupFactory.create(type_id='area'))
chair = RoleFactory(group=group, name_id='chair').person
url = urlreverse('ietf.group.views.edit', kwargs=dict(group_type=group.type_id, acronym=group.acronym, action='edit'))
# parent is not shown to group chair
login_testing_unauthorized(self, chair.user.username, url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('form select[name=parent]')), 0)
# view ignores attempt to change parent
old_parent = group.parent
new_parent = GroupFactory(type_id='area')
self.assertNotEqual(new_parent.acronym, group.parent.acronym)
r = self.client.post(url, dict(
name=group.name,
acronym=group.acronym,
state=group.state_id,
parent=new_parent.pk))
self.assertEqual(r.status_code, 302)
group = Group.objects.get(pk=group.pk)
self.assertNotEqual(group.parent, new_parent)
self.assertEqual(group.parent, old_parent)
# parent is shown to AD and Secretariat
for priv_user in ('ad', 'secretary'):
self.client.logout()
login_testing_unauthorized(self, priv_user, url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('form select[name=parent]')), 1)
new_parent = GroupFactory(type_id='area')
self.assertNotEqual(new_parent.acronym, group.parent.acronym)
r = self.client.post(url, dict(
name=group.name,
acronym=group.acronym,
state=group.state_id,
parent=new_parent.pk))
self.assertEqual(r.status_code, 302)
group = Group.objects.get(pk=group.pk)
self.assertEqual(group.parent, new_parent)
def test_edit_parent_field(self):
group = GroupFactory.create(type_id='wg', parent=GroupFactory.create(type_id='area'))
chair = RoleFactory(group=group, name_id='chair').person
url = urlreverse('ietf.group.views.edit', kwargs=dict(group_type=group.type_id, acronym=group.acronym, action='edit', field='parent'))
# parent is not shown to group chair
login_testing_unauthorized(self, chair.user.username, url)
r = self.client.get(url)
self.assertEqual(r.status_code, 403)
# chair is not allowed to change parent
new_parent = GroupFactory(type_id='area')
self.assertNotEqual(new_parent.acronym, group.parent.acronym)
r = self.client.post(url, dict(parent=new_parent.pk))
self.assertEqual(r.status_code, 403)
# parent is shown to AD and Secretariat
for priv_user in ('ad', 'secretary'):
self.client.logout()
login_testing_unauthorized(self, priv_user, url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('form select[name=parent]')), 1)
new_parent = GroupFactory(type_id='area')
self.assertNotEqual(new_parent.acronym, group.parent.acronym)
r = self.client.post(url, dict(parent=new_parent.pk))
self.assertEqual(r.status_code, 302)
group = Group.objects.get(pk=group.pk)
self.assertEqual(group.parent, new_parent)
def test_conclude(self):
group = GroupFactory(acronym="mars")
url = urlreverse('ietf.group.views.conclude', kwargs=dict(group_type=group.type_id, acronym=group.acronym))
login_testing_unauthorized(self, "secretary", url)
# normal get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('form textarea[name=instructions]')), 1)
# faulty post
r = self.client.post(url, dict(instructions="")) # No instructions
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form .is-invalid')) > 0)
# request conclusion
mailbox_before = len(outbox)
r = self.client.post(url, dict(instructions="Test instructions"))
self.assertEqual(r.status_code, 302)
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue('iesg-secretary@' in outbox[-1]['To'])
# the WG remains active until the Secretariat takes action
group = Group.objects.get(acronym=group.acronym)
self.assertEqual(group.state_id, "active")
def test_replace(self):
group = GroupFactory(state_id='bof')
charter = CharterFactory(group=group, states=[('charter','intrev')])
BallotDocEventFactory(doc=charter, ballot_type__doc_type_id='draft', ballot_type__slug='r-extrev')
url = urlreverse('ietf.group.views.edit', kwargs=dict(group_type=group.type_id, acronym=group.acronym, action="edit", field="state"))
self.client.login(username='secretary',password='secretary+password')
self.client.post(url, dict(state='replaced'))
group = Group.objects.get(pk=group.pk)
self.assertEqual(group.state_id, 'replaced')
self.assertEqual(group.charter.get_state_slug('charter'), 'replaced')
self.assertEqual(group.charter.active_ballot(), None)
def test_add_comment(self):
group = GroupFactory(acronym="mars",parent=GroupFactory(type_id='area'))
RoleFactory(group=group,person=Person.objects.get(user__username='ad'),name_id='ad')
RoleFactory(group=group,person__user__username='marschairman',name_id='chair')
RoleFactory(group=group,person__user__username='marssecretary',name_id='secr')
RoleFactory(group=group,person__user__username='marsdelegate',name_id='delegate')
url = urlreverse('ietf.group.views.add_comment', kwargs=dict(acronym=group.acronym))
empty_outbox()
for username in ['secretary','ad','marschairman','marssecretary','marsdelegate']:
login_testing_unauthorized(self, username, url)
# get
r = self.client.get(url)
self.assertContains(r, "Add comment")
self.assertContains(r, group.acronym)
q = PyQuery(r.content)
self.assertEqual(len(q('form textarea[name=comment]')), 1)
# post
r = self.client.post(url, dict(comment="Test comment %s"%username))
self.assertEqual(r.status_code, 302)
person = Person.objects.get(user__username=username)
self.assertTrue(GroupEvent.objects.filter(group=group,by=person,type='added_comment',desc='Test comment %s'%username).exists())
self.client.logout()
self.client.login(username='ameschairman',password='ameschairman+password')
r=self.client.get(url)
self.assertEqual(r.status_code,403)
self.assertEqual(len(outbox),5)
class GroupFormTests(TestCase):
"""Tests of the GroupForm form"""
@staticmethod
def _format_resource(r):
if r.display_name:
return '{} {} ({})'.format(r.name.slug, r.value, r.display_name.strip('()'))
else:
return '{} {}'.format(r.name.slug, r.value)
def _group_post_data(self, group):
data=dict(
name=group.name,
acronym=group.acronym,
state=group.state_id,
parent=group.parent_id or '',
list_email=group.list_email if group.list_email else None,
list_subscribe=group.list_subscribe if group.list_subscribe else '',
list_archive=group.list_archive if group.list_archive else '',
resources='\n'.join(self._format_resource(r) for r in group.groupextresource_set.all()),
closing_note='', # not a group attribute, handled specially by the view; ignore in this test
)
# fill in original values
for rslug in group.get_used_roles():
data['{}_roles'.format(rslug)] = list(group.role_set.filter(name_id=rslug).values_list('email__address', flat=True).all())
return data
def _assert_cleaned_data_equal(self, cleaned_data, post_data):
for attr, expected in post_data.items():
value = cleaned_data[attr]
if attr.endswith('_roles'):
actual = list(value.values_list('address', flat=True).all())
elif attr == 'resources':
# must handle resources specially
actual = '\n'.join(self._format_resource(r) for r in value)
elif hasattr(value, 'pk'):
actual = value.pk
else:
actual = '' if value is None else value
self.assertEqual(actual, expected, 'unexpected value for {}'.format(attr))
def do_edit_roles_test(self, group):
# get post_data for the group
orig_data = self._group_post_data(group)
# create a user to be assigned roles
new_email = EmailFactory()
# Now check that we can replace each used_role without disturbing the others.
# This does not actually update group, so start with orig_data each time.
for rslug in group.get_used_roles():
data = orig_data.copy()
edit_field = '{}_roles'.format(rslug)
data[edit_field] = [new_email.address]
form = GroupForm(data, group=group, group_type=group.type_id, field=None)
self.assertTrue(form.is_valid())
# Check that all cleaned values match what we passed to the form.
self._assert_cleaned_data_equal(form.cleaned_data, data)
def test_edit_roles(self):
"""Test that roles can be edited for all group types
N.B., the combinations of group type and parent group and the used_roles are
obtained from the GroupFeatures in the database. The handling of these combinations
is validated, but this test cannot check that the rules themselves are correct.
As long as names.json is up to date, this will test what we want.
"""
# Test every parent type that is allowed for at least one group type
for parent_type in GroupTypeName.objects.filter(child_features__isnull=False).distinct():
parent = GroupFactory(type_id=parent_type.pk)
for child_features in parent_type.child_features.all():
# create a group of each child type for this parent and populate its roles
group_type = child_features.type
group = GroupFactory(type_id=group_type.pk, parent=parent)
for rslug in group.get_used_roles():
RoleFactory(name_id=rslug, group=group, person=PersonFactory())
self.do_edit_roles_test(group)
def test_used_roles_overrides_default(self):
"""A group's used_roles should override the GroupFeatures default"""
request_factory = RequestFactory()
group = GroupFactory(type_id='wg')
self.assertEqual(GroupForm(request_factory.get('/some/url'), group=group).used_roles, group.features.default_used_roles)
group.used_roles = ['ad', 'chair']
group.save()
self.assertNotEqual(group.used_roles, group.features.default_used_roles)
self.assertEqual(GroupForm(request_factory.get('/some/url'), group=group).used_roles, group.used_roles)
def test_need_parent(self):
"""GroupForm should enforce non-null parent when required"""
group = GroupFactory()
parent = group.parent
other_parent = GroupFactory(type_id=parent.type_id)
for rslug in group.get_used_roles():
RoleFactory(name_id=rslug, group=group, person=PersonFactory())
data = self._group_post_data(group)
# First, test with parent required
group.type.features.need_parent = True
group.type.features.save()
group = Group.objects.get(pk=group.pk) # renew object to clear features cache
# should fail with empty parent
data['parent'] = ''
form = GroupForm(data, group=group, group_type=group.type_id, field=None)
self.assertFalse(form.is_valid()) # cannot update to empty parent
# should succeed with non-empty parent
data['parent'] = other_parent.pk
form = GroupForm(data, group=group, group_type=group.type_id, field=None)
self.assertTrue(form.is_valid())
self._assert_cleaned_data_equal(form.cleaned_data, data)
# Second, test with parent not required
group.type.features.need_parent = False
group.type.features.save()
group = Group.objects.get(pk=group.pk) # renew object to clear features cache
# should succeed with empty parent
data['parent'] = ''
form = GroupForm(data, group=group, group_type=group.type_id, field=None)
self.assertTrue(form.is_valid())
self._assert_cleaned_data_equal(form.cleaned_data, data)
# should succeed with non-empty parent
data['parent'] = other_parent.pk
form = GroupForm(data, group=group, group_type=group.type_id, field=None)
self.assertTrue(form.is_valid())
self._assert_cleaned_data_equal(form.cleaned_data, data)
def test_no_description_field_for_chartered_groups(self):
group = GroupFactory()
self.assertTrue(
group.features.has_chartering_process,
'Group type must have has_chartering_process=True for this test',
)
self.assertNotIn('description', GroupForm(group=group).fields)
self.assertNotIn('description', GroupForm(group_type=group.type).fields)
self.assertNotIn('description', GroupForm(group=group, group_type=group.type).fields)
self.assertNotIn('description', GroupForm(data={'description': 'blah'}, group=group).fields)
self.assertNotIn('description', GroupForm(data={'description': 'blah'}, group_type=group.type).fields)
self.assertNotIn('description', GroupForm(data={'description': 'blah'}, group=group, group_type=group.type).fields)
def test_have_description_field_for_non_chartered_groups(self):
group = GroupFactory(type_id='dir')
self.assertFalse(
group.features.has_chartering_process,
'Group type must have has_chartering_process=False for this test',
)
self.assertIn('description', GroupForm(group=group).fields)
self.assertIn('description', GroupForm(group_type=group.type).fields)
self.assertIn('description', GroupForm(group=group, group_type=group.type).fields)
self.assertIn('description', GroupForm(data={'description': 'blah'}, group=group).fields)
self.assertIn('description', GroupForm(data={'description': 'blah'}, group_type=group.type).fields)
self.assertIn('description', GroupForm(data={'description': 'blah'}, group=group, group_type=group.type).fields)
class MilestoneTests(TestCase):
def create_test_milestones(self):
group = GroupFactory(acronym='mars',parent=GroupFactory(type_id='area'),list_email='mars-wg@ietf.org')
CharterFactory(group=group)
RoleFactory(group=group,name_id='ad',person=Person.objects.get(user__username='ad'))
RoleFactory(group=group,name_id='chair',person=PersonFactory(user__username='marschairman'))
draft = WgDraftFactory(group=group)
m1 = GroupMilestone.objects.create(
group=group,
desc="Test 1",
due=date_today(DEADLINE_TZINFO),
resolved="",
state_id="active")
m1.docs.set([draft])
m2 = GroupMilestone.objects.create(
group=group,
desc="Test 2",
due=date_today(DEADLINE_TZINFO),
resolved="",
state_id="charter")
m2.docs.set([draft])
return (m1, m2, group)
def last_day_of_month(self, d):
return datetime.date(d.year, d.month, calendar.monthrange(d.year, d.month)[1])
def test_milestone_sets(self):
m1, m2, group = self.create_test_milestones()
for url in group_urlreverse_list(group, 'ietf.group.milestones.edit_milestones;current'):
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertContains(r, m1.desc)
self.assertNotContains(r, m2.desc)
self.client.logout()
login_testing_unauthorized(self, "secretary", url)
for url in group_urlreverse_list(group, 'ietf.group.milestones.edit_milestones;charter'):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertNotContains(r, m1.desc)
self.assertContains(r, m2.desc)
def test_add_milestone(self):
m1, m2, group = self.create_test_milestones()
url = urlreverse('ietf.group.milestones.edit_milestones;current', kwargs=dict(group_type=group.type_id, acronym=group.acronym))
login_testing_unauthorized(self, "secretary", url)
# normal get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
milestones_before = GroupMilestone.objects.count()
events_before = group.groupevent_set.count()
doc_pks = pklist(Document.objects.filter(type="draft"))
due = self.last_day_of_month(date_today(DEADLINE_TZINFO) + datetime.timedelta(days=365))
# faulty post
r = self.client.post(url, { 'prefix': "m-1",
'm-1-id': "-1",
'm-1-desc': "", # no description
'm-1-due': due.strftime("%B %Y"),
'm-1-resolved': "",
'm-1-docs': doc_pks,
'action': "save",
})
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form .is-invalid')) > 0)
self.assertEqual(GroupMilestone.objects.count(), milestones_before)
# add
mailbox_before = len(outbox)
r = self.client.post(url, { 'prefix': "m-1",
'm-1-id': "-1",
'm-1-desc': "Test 3",
'm-1-due': due.strftime("%B %Y"),
'm-1-resolved': "",
'm-1-docs': doc_pks,
'action': "save",
})
self.assertEqual(r.status_code, 302)
self.assertEqual(GroupMilestone.objects.count(), milestones_before + 1)
self.assertEqual(group.groupevent_set.count(), events_before + 1)
m = GroupMilestone.objects.get(desc="Test 3")
self.assertEqual(m.state_id, "active")
self.assertEqual(m.due, due)
self.assertEqual(m.resolved, "")
self.assertEqual(set(pklist(m.docs)), set(doc_pks))
self.assertTrue("Added milestone" in m.milestonegroupevent_set.all()[0].desc)
self.assertEqual(len(outbox),mailbox_before+2)
self.assertFalse(any('Review Required' in x['Subject'] for x in outbox[-2:]))
self.assertTrue('Milestones changed' in outbox[-2]['Subject'])
self.assertTrue('mars-chairs@' in outbox[-2]['To'])
self.assertTrue('aread@' in outbox[-2]['To'])
self.assertTrue('Milestones changed' in outbox[-1]['Subject'])
self.assertTrue('mars-wg@' in outbox[-1]['To'])
def test_add_milestone_as_chair(self):
m1, m2, group = self.create_test_milestones()
url = urlreverse('ietf.group.milestones.edit_milestones;current', kwargs=dict(group_type=group.type_id, acronym=group.acronym))
login_testing_unauthorized(self, "marschairman", url)
# normal get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
milestones_before = GroupMilestone.objects.filter(group=group).count()
events_before = group.groupevent_set.count()
due = self.last_day_of_month(date_today(DEADLINE_TZINFO) + datetime.timedelta(days=365))
# add
mailbox_before = len(outbox)
r = self.client.post(url, { 'prefix': "m-1",
'm-1-id': -1,
'm-1-desc': "Test 3",
'm-1-due': due.strftime("%B %Y"),
'm-1-resolved': "",
'action': "save",
})
self.assertEqual(r.status_code, 302)
m = GroupMilestone.objects.filter(group=group)
self.assertEqual(m.count(), milestones_before + 1)
m = GroupMilestone.objects.get(desc="Test 3")
self.assertEqual(m.state_id, "review")
self.assertEqual(group.groupevent_set.count(), events_before + 1)
self.assertTrue("for review" in m.milestonegroupevent_set.all()[0].desc)
self.assertEqual(len(outbox),mailbox_before+1)
self.assertTrue('Review Required' in outbox[-1]['Subject'])
self.assertFalse(group.list_email in outbox[-1]['To'])
def test_accept_milestone(self):
m1, m2, group = self.create_test_milestones()
m1.state_id = "review"
m1.save()
url = urlreverse('ietf.group.milestones.edit_milestones;current', kwargs=dict(group_type=group.type_id, acronym=group.acronym))
login_testing_unauthorized(self, "ad", url)
# normal get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
events_before = group.groupevent_set.count()
# add
mstr = f"m{m1.id}"
r = self.client.post(url, { 'prefix': mstr,
f'{mstr}-id': m1.id,
f'{mstr}-desc': m1.desc,
f'{mstr}-due': m1.due.strftime("%B %Y"),
f'{mstr}-resolved': m1.resolved,
f'{mstr}-docs': pklist(m1.docs),
f'{mstr}-review': "accept",
'action': "save",
})
self.assertEqual(r.status_code, 302)
m = GroupMilestone.objects.get(pk=m1.pk)
self.assertEqual(m.state_id, "active")
self.assertEqual(group.groupevent_set.count(), events_before + 1)
self.assertTrue("to active from review" in m.milestonegroupevent_set.all()[0].desc)
def test_delete_milestone(self):
m1, m2, group = self.create_test_milestones()
url = urlreverse('ietf.group.milestones.edit_milestones;current', kwargs=dict(group_type=group.type_id, acronym=group.acronym))
login_testing_unauthorized(self, "secretary", url)
milestones_before = GroupMilestone.objects.count()
events_before = group.groupevent_set.count()
# delete
mstr = f"m{m1.id}"
r = self.client.post(url, { 'prefix': mstr,
f'{mstr}-id': m1.id,
f'{mstr}-desc': m1.desc,
f'{mstr}-due': m1.due.strftime("%B %Y"),
f'{mstr}-resolved': "",
f'{mstr}-docs': pklist(m1.docs),
f'{mstr}-delete': "checked",
'action': "save",
})
self.assertEqual(r.status_code, 302)
self.assertEqual(GroupMilestone.objects.count(), milestones_before)
self.assertEqual(group.groupevent_set.count(), events_before + 1)
m = GroupMilestone.objects.get(pk=m1.pk)
self.assertEqual(m.state_id, "deleted")
self.assertTrue("Deleted milestone" in m.milestonegroupevent_set.all()[0].desc)
def test_edit_milestone(self):
m1, m2, group = self.create_test_milestones()
url = urlreverse('ietf.group.milestones.edit_milestones;current', kwargs=dict(group_type=group.type_id, acronym=group.acronym))
login_testing_unauthorized(self, "secretary", url)
milestones_before = GroupMilestone.objects.count()
events_before = group.groupevent_set.count()
doc_pks = pklist(Document.objects.filter(type="draft"))
due = self.last_day_of_month(date_today(DEADLINE_TZINFO) + datetime.timedelta(days=365))
mstr = f"m{m1.id}"
# faulty post
r = self.client.post(url, { 'prefix': mstr,
f'{mstr}-id': m1.id,
f'{mstr}-desc': "", # no description
f'{mstr}-due': due.strftime("%B %Y"),
f'{mstr}-resolved': "",
f'{mstr}-docs': doc_pks,
'action': "save",
})
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form .is-invalid')) > 0)
m = GroupMilestone.objects.get(pk=m1.pk)
self.assertEqual(GroupMilestone.objects.count(), milestones_before)
self.assertEqual(m.due, m1.due)
# edit
mailbox_before = len(outbox)
r = self.client.post(url, { 'prefix': mstr,
f'{mstr}-id': m1.id,
f'{mstr}-desc': "Test 2 - changed",
f'{mstr}-due': due.strftime("%B %Y"),
f'{mstr}-resolved': "Done",
f'{mstr}-resolved_checkbox': "checked",
f'{mstr}-docs': doc_pks,
'action': "save",
})
self.assertEqual(r.status_code, 302)
self.assertEqual(GroupMilestone.objects.count(), milestones_before)
self.assertEqual(group.groupevent_set.count(), events_before + 1)
m = GroupMilestone.objects.get(pk=m1.pk)
self.assertEqual(m.state_id, "active")
self.assertEqual(m.due, due)
self.assertEqual(m.resolved, "Done")
self.assertEqual(set(pklist(m.docs)), set(doc_pks))
self.assertTrue("Changed milestone" in m.milestonegroupevent_set.all()[0].desc)
self.assertEqual(len(outbox), mailbox_before + 2)
self.assertTrue("Milestones changed" in outbox[-2]["Subject"])
self.assertTrue(group.ad_role().email.address in str(outbox[-2]))
self.assertTrue("Milestones changed" in outbox[-1]["Subject"])
self.assertTrue(group.list_email in str(outbox[-1]))
def test_reset_charter_milestones(self):
m1, m2, group = self.create_test_milestones()
url = urlreverse('ietf.group.milestones.reset_charter_milestones', kwargs=dict(group_type=group.type_id, acronym=group.acronym))
login_testing_unauthorized(self, "secretary", url)
# normal get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(q('input[name=milestone]').val(), str(m1.pk))
events_before = group.charter.docevent_set.count()
# reset
r = self.client.post(url, dict(milestone=[str(m1.pk)]))
self.assertEqual(r.status_code, 302)
self.assertEqual(GroupMilestone.objects.get(pk=m1.pk).state_id, "active")
self.assertEqual(GroupMilestone.objects.get(pk=m2.pk).state_id, "deleted")
self.assertEqual(GroupMilestone.objects.filter(due=m1.due, desc=m1.desc, state="charter").count(), 1)
self.assertEqual(group.charter.docevent_set.count(), events_before + 2) # 1 delete, 1 add
def test_edit_sort(self):
group = GroupFactory(uses_milestone_dates=False)
DatelessGroupMilestoneFactory(group=group,order=1)
DatelessGroupMilestoneFactory(group=group,order=0)
DatelessGroupMilestoneFactory(group=group,order=None)
url = urlreverse('ietf.group.milestones.edit_milestones;current', kwargs=dict(group_type=group.type_id, acronym=group.acronym))
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual([x.value for x in q('input[id^=id_m][id$=order]')], [None, '0', '1'])
class DatelessMilestoneTests(TestCase):
def test_switch_to_dateless(self):
ad_role = RoleFactory(group__type_id='area',name_id='ad')
ms = DatedGroupMilestoneFactory(group__parent=ad_role.group)
ad = ad_role.person
chair = RoleFactory(group=ms.group,name_id='chair').person
url = urlreverse('ietf.group.milestones.edit_milestones;current', kwargs=dict(acronym=ms.group.acronym))
login_testing_unauthorized(self, chair.user.username, url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('button[value="switch"]:submit')),0)
r = self.client.post(url, dict(action="switch"))
self.assertEqual(r.status_code, 403)
self.client.logout()
self.client.login(username=ad.user.username, password='%s+password' % ad.user.username)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('#switch-date-use-form')),1)
self.assertEqual(len(q('#uses_milestone_dates')),1)
r = self.client.post(url, dict(action="switch"))
self.assertEqual(r.status_code, 200)
ms = GroupMilestone.objects.get(id=ms.id)
self.assertFalse(ms.group.uses_milestone_dates)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('#uses_milestone_dates')),0)
def test_switch_to_dated(self):
ad_role = RoleFactory(group__type_id='area',name_id='ad')
ms = DatelessGroupMilestoneFactory(group__parent=ad_role.group)
ad = ad_role.person
url = urlreverse('ietf.group.milestones.edit_milestones;current', kwargs=dict(acronym=ms.group.acronym))
login_testing_unauthorized(self, ad.user.username, url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('#uses_milestone_dates')),0)
r = self.client.post(url, dict(action="switch"))
self.assertEqual(r.status_code, 200)
ms = GroupMilestone.objects.get(id=ms.id)
self.assertTrue(ms.group.uses_milestone_dates)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('#uses_milestone_dates')),1)
def test_add_first_milestone(self):
role = RoleFactory(name_id='chair',group__uses_milestone_dates=False)
group = role.group
chair = role.person
url = urlreverse('ietf.group.milestones.edit_milestones;current', kwargs=dict(acronym=group.acronym))
login_testing_unauthorized(self, chair.user.username, url)
r = self.client.post(url, { 'prefix': "m-1",
'm-1-id': -1,
'm-1-desc': "Test 3",
'm-1-order': 1,
'm-1-resolved': "",
'action': "save",
})
self.assertEqual(r.status_code, 302)
self.assertEqual(group.groupmilestone_set.count(),1)
def test_can_switch_date_types_for_initial_charter(self):
ad_role = RoleFactory(group__type_id='area',name_id='ad')
ms = DatedGroupMilestoneFactory(group__parent=ad_role.group)
ad = ad_role.person
ms.group.charter = CharterFactory(group=ms.group)
url = urlreverse('ietf.group.milestones.edit_milestones;charter', kwargs=dict(acronym=ms.group.acronym))
login_testing_unauthorized(self, ad.user.username, url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(q('#switch-date-use-form button').hasClass('d-none'))
ms.group.charter.rev='00-00'
ms.group.charter.save()
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(q('#switch-date-use-form button').attr('style'), None)
def test_edit_and_reorder_milestone(self):
role = RoleFactory(name_id='chair',group__uses_milestone_dates=False)
group = role.group
DatelessGroupMilestoneFactory.create_batch(3,group=group)
url = urlreverse('ietf.group.milestones.edit_milestones;current', kwargs=dict(acronym=group.acronym))
login_testing_unauthorized(self, "secretary", url)
post_data = dict()
prefixes = []
for ms in group.groupmilestone_set.order_by('order'):
prefix = 'm%d' % ms.id
prefixes.append(prefix)
post_data['%s-id' % prefix] = ms.id
post_data['%s-desc' % prefix] = ms.desc
post_data['%s-order' % prefix] = ms.order
post_data['%s-docs' % prefix] = []
post_data['prefix'] = prefixes
post_data['action'] = 'review'
# Change the second milestone's description
post_data['%s-desc' % prefixes[1]] = '2s09dhfbn23tn'
# Switch the order of the first and second milestone
post_data['%s-order' % prefixes[0]] = 2
post_data['%s-order' % prefixes[1]] = 1
r = self.client.post(url, post_data)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('span.badge:contains("Changed")')), 2)
post_data['action'] = 'save'
r = self.client.post(url, post_data)
self.assertEqual(r.status_code, 302)
milestones = group.groupmilestone_set.order_by('order')
self.assertEqual(milestones[0].desc,'2s09dhfbn23tn')
class CustomizeWorkflowTests(TestCase):
def test_customize_workflow(self):
group = GroupFactory()
url = urlreverse('ietf.group.views.customize_workflow', kwargs=dict(group_type=group.type_id, acronym=group.acronym))
login_testing_unauthorized(self, "secretary", url)
state = State.objects.get(used=True, type="draft-stream-ietf", slug="wg-lc")
self.assertTrue(state not in group.unused_states.all())
# get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q("form.set-state").find("input[name=state][value='%s']" % state.pk).parents("form").find("input[name=active][value='0']")), 1)
# deactivate state
r = self.client.post(url,
dict(action="setstateactive",
state=state.pk,
active="0"))
self.assertEqual(r.status_code, 302)
r = self.client.get(url)
q = PyQuery(r.content)
self.assertEqual(len(q("form.set-state").find("input[name=state][value='%s']" % state.pk).parents("form").find("input[name=active][value='1']")), 1)
group = Group.objects.get(acronym=group.acronym)
self.assertTrue(state in group.unused_states.all())
# change next states
state = State.objects.get(used=True, type="draft-stream-ietf", slug="wg-doc")
next_states = State.objects.filter(used=True, type=b"draft-stream-ietf", slug__in=["parked", "dead", "wait-wgw", 'sub-pub']).values_list('pk', flat=True)
r = self.client.post(url,
dict(action="setnextstates",
state=state.pk,
next_states=next_states))
self.assertEqual(r.status_code, 302)
r = self.client.get(url)
q = PyQuery(r.content)
self.assertEqual(len(q("form.set-next-states").find("input[name=state][value='%s']" % state.pk).parents('form').find("input[name=next_states][checked=checked]")), len(next_states))
transitions = GroupStateTransitions.objects.filter(group=group, state=state)
self.assertEqual(len(transitions), 1)
self.assertEqual(set(transitions[0].next_states.values_list("pk", flat=True)), set(next_states))
# change them back to default
next_states = state.next_states.values_list("pk", flat=True)
r = self.client.post(url,
dict(action="setnextstates",
state=state.pk,
next_states=next_states))
self.assertEqual(r.status_code, 302)
r = self.client.get(url)
q = PyQuery(r.content)
transitions = GroupStateTransitions.objects.filter(group=group, state=state)
self.assertEqual(len(transitions), 0)
# deactivate tag
tag = DocTagName.objects.get(slug="w-expert")
r = self.client.post(url,
dict(action="settagactive",
tag=tag.pk,
active="0"))
self.assertEqual(r.status_code, 302)
r = self.client.get(url)
q = PyQuery(r.content)
self.assertEqual(len(q('form').find('input[name=tag][value="%s"]' % tag.pk).parents("form").find("input[name=active]")), 1)
group = Group.objects.get(acronym=group.acronym)
self.assertTrue(tag in group.unused_tags.all())
class EmailAliasesTests(TestCase):
def setUp(self):
super().setUp()
PersonFactory(user__username='plain')
GroupFactory(acronym='mars',parent=GroupFactory(type_id='area'))
GroupFactory(acronym='ames',parent=GroupFactory(type_id='area'))
@mock.patch("ietf.group.views.get_group_email_aliases")
def testAliases(self, mock_get_aliases):
url = urlreverse('ietf.group.urls_info_details.redirect.email', kwargs=dict(acronym="mars"))
r = self.client.get(url)
self.assertEqual(r.status_code, 302)
mock_get_aliases.return_value = [
{"acronym": "mars", "alias_type": "-ads", "expansion": "aread@example.org"},
{"acronym": "mars", "alias_type": "-chairs", "expansion": "mars_chair@ietf.org"},
]
for testdict in [dict(acronym="mars"),dict(acronym="mars",group_type="wg")]:
url = urlreverse('ietf.group.urls_info_details.redirect.email', kwargs=testdict)
r = self.client.get(url,follow=True)
self.assertEqual(
mock_get_aliases.call_args,
mock.call(testdict.get("acronym", None), testdict.get("group_type", None)),
)
self.assertTrue(all([x in unicontent(r) for x in ['mars-ads@','mars-chairs@']]))
self.assertFalse(any([x in unicontent(r) for x in ['ames-ads@','ames-chairs@']]))
url = urlreverse('ietf.group.views.email_aliases', kwargs=dict())
login_testing_unauthorized(self, "plain", url)
mock_get_aliases.return_value = [
{"acronym": "mars", "alias_type": "-ads", "expansion": "aread@example.org"},
{"acronym": "mars", "alias_type": "-chairs", "expansion": "mars_chair@ietf.org"},
{"acronym": "ames", "alias_type": "-ads", "expansion": "aread@example.org"},
{"acronym": "ames", "alias_type": "-chairs", "expansion": "mars_chair@ietf.org"},
]
r = self.client.get(url)
self.assertTrue(r.status_code,200)
self.assertEqual(mock_get_aliases.call_args, mock.call(None, None))
self.assertTrue(all([x in unicontent(r) for x in ['mars-ads@','mars-chairs@','ames-ads@','ames-chairs@']]))
url = urlreverse('ietf.group.views.email_aliases', kwargs=dict(group_type="wg"))
mock_get_aliases.return_value = [
{"acronym": "mars", "alias_type": "-ads", "expansion": "aread@example.org"},
{"acronym": "mars", "alias_type": "-chairs", "expansion": "mars_chair@ietf.org"},
{"acronym": "ames", "alias_type": "-ads", "expansion": "aread@example.org"},
{"acronym": "ames", "alias_type": "-chairs", "expansion": "mars_chair@ietf.org"},
]
r = self.client.get(url)
self.assertEqual(r.status_code,200)
self.assertEqual(mock_get_aliases.call_args, mock.call(None, "wg"))
self.assertContains(r, 'mars-ads@')
url = urlreverse('ietf.group.views.email_aliases', kwargs=dict(group_type="rg"))
mock_get_aliases.return_value = []
r = self.client.get(url)
self.assertEqual(r.status_code,200)
self.assertEqual(mock_get_aliases.call_args, mock.call(None, "rg"))
self.assertNotContains(r, 'mars-ads@')
@mock.patch("ietf.group.views.get_group_email_aliases")
def testExpansions(self, mock_get_aliases):
mock_get_aliases.return_value = [
{"acronym": "mars", "alias_type": "-ads", "expansion": "aread@example.org"},
{"acronym": "mars", "alias_type": "-chairs", "expansion": "mars_chair@ietf.org"},
{"acronym": "ames", "alias_type": "-ads", "expansion": "aread@example.org"},
{"acronym": "ames", "alias_type": "-chairs", "expansion": "mars_chair@ietf.org"},
]
url = urlreverse('ietf.group.views.email', kwargs=dict(acronym="mars"))
r = self.client.get(url)
self.assertEqual(r.status_code,200)
self.assertEqual(mock_get_aliases.call_args, mock.call("mars", None))
self.assertContains(r, 'Email aliases')
self.assertContains(r, 'mars-ads@ietf.org')
self.assertContains(r, 'group_personnel_change')
class AjaxTests(TestCase):
def test_group_menu_data(self):
GroupFactory(acronym='mars',parent=Group.objects.get(acronym='farfut'))
r = self.client.get(urlreverse('ietf.group.views.group_menu_data'))
self.assertEqual(r.status_code, 200)
parents = r.json()
area = Group.objects.get(type="area", acronym="farfut")
self.assertTrue(str(area.id) in parents)
mars_wg_data = None
for g in parents[str(area.id)]:
if g["acronym"] == "mars":
mars_wg_data = g
break
self.assertTrue(mars_wg_data)
mars_wg = Group.objects.get(acronym="mars")
self.assertEqual(mars_wg_data["name"], mars_wg.name)
class MeetingInfoTests(TestCase):
def setUp(self):
super().setUp()
self.group = GroupFactory.create(type_id='wg')
today = date_today()
SessionFactory.create(meeting__type_id='ietf',group=self.group,meeting__date=today-datetime.timedelta(days=14))
self.inprog = SessionFactory.create(meeting__type_id='ietf',group=self.group,meeting__date=today-datetime.timedelta(days=1))
SessionFactory.create(meeting__type_id='ietf',group=self.group,meeting__date=today+datetime.timedelta(days=90))
SessionFactory.create(meeting__type_id='interim',group=self.group,meeting__date=today+datetime.timedelta(days=45))
def test_meeting_info(self):
for url in group_urlreverse_list(self.group, 'ietf.group.views.meetings'):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
q = PyQuery(response.content)
self.assertTrue(q('#inprogressmeets'))
self.assertTrue(q('#futuremeets'))
self.assertTrue(q('#pastmeets'))
# This should be improved to make sure each session shows up in the right section above
for session in self.group.session_set.all():
self.assertIn(session.meeting.number,unicontent(response))
self.group.session_set.filter(id=self.inprog.id).delete()
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
q = PyQuery(response.content)
self.assertFalse(q('#inprogressmeets'))
class StatusUpdateTests(TestCase):
def test_unsupported_group_types(self):
def ensure_updates_dont_show(group, user):
url = urlreverse('ietf.group.views.group_about',kwargs={'acronym':group.acronym})
if user:
self.client.login(username=user.username,password='%s+password'%user.username)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
q = PyQuery(response.content)
self.assertFalse(q('tr#status_update') )
self.client.logout()
def ensure_cant_edit(group,user):
url = urlreverse('ietf.group.views.group_about_status_edit',kwargs={'acronym':group.acronym})
if user:
self.client.login(username=user.username,password='%s+password'%user.username)
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
self.client.logout()
for type_id in GroupTypeName.objects.exclude(slug__in=('wg','rg','ag','rag','team','edwg')).values_list('slug',flat=True):
group = GroupFactory.create(type_id=type_id)
for user in (None,User.objects.get(username='secretary')):
ensure_updates_dont_show(group,user)
ensure_cant_edit(group,user)
def test_see_status_update(self):
chair = RoleFactory(name_id='chair',group__type_id='wg')
GroupEventFactory(type='status_update',group=chair.group)
for url in group_urlreverse_list(chair.group, 'ietf.group.views.group_about'):
response = self.client.get(url)
self.assertEqual(response.status_code,200)
q=PyQuery(response.content)
self.assertTrue(q('tr#status_update'))
self.assertTrue(q('tr#status_update td a:contains("Show")'))
self.assertFalse(q('tr#status_update td a:contains("Edit")'))
self.client.login(username=chair.person.user.username,password='%s+password'%chair.person.user.username)
response = self.client.get(url)
self.assertEqual(response.status_code,200)
q=PyQuery(response.content)
self.assertTrue(q('tr#status_update td a:contains("Show")'))
self.assertTrue(q('tr#status_update td a:contains("Edit")'))
self.client.logout()
def test_view_status_update(self):
chair = RoleFactory(name_id='chair',group__type_id='wg')
event = GroupEventFactory(type='status_update',group=chair.group)
for url in group_urlreverse_list(chair.group, 'ietf.group.views.group_about_status'):
response = self.client.get(url)
self.assertEqual(response.status_code,200)
q=PyQuery(response.content)
self.assertTrue(bleach.linkify(escape(event.desc), parse_email=True) in str(q('pre')))
self.assertFalse(q('a#edit_button'))
self.client.login(username=chair.person.user.username,password='%s+password'%chair.person.user.username)
response = self.client.get(url)
self.assertEqual(response.status_code,200)
q=PyQuery(response.content)
self.assertTrue(q('a#edit_button'))
self.client.logout()
def test_edit_status_update(self):
chair = RoleFactory(name_id='chair',group__type_id='wg')
event = GroupEventFactory(type='status_update',group=chair.group)
url = urlreverse('ietf.group.views.group_about_status_edit',kwargs={'acronym':chair.group.acronym})
response = self.client.get(url)
self.assertEqual(response.status_code,404)
self.client.login(username=chair.person.user.username,password='%s+password'%chair.person.user.username)
response = self.client.get(url)
self.assertEqual(response.status_code,200)
q=PyQuery(response.content)
self.assertTrue(event.desc in q('form textarea#id_content').text())
response = self.client.post(url,dict(content='Direct content typed into form',submit_response='1'))
self.assertEqual(response.status_code, 302)
self.assertEqual(chair.group.latest_event(type='status_update').desc,'Direct content typed into form')
test_file = io.StringIO("This came from a file.")
test_file.name = "unnamed"
response = self.client.post(url,dict(txt=test_file,submit_response="1"))
self.assertEqual(response.status_code, 302)
self.assertEqual(chair.group.latest_event(type='status_update').desc,'This came from a file.')
def test_view_all_status_updates(self):
area = GroupFactory(type_id='area')
wg = GroupFactory(type_id='wg',parent=area)
irtf = GroupFactory(type_id='irtf')
rg = GroupFactory(type_id='rg',parent=irtf)
GroupEventFactory(type='status_update',desc='blah blah blah',group=wg)
GroupEventFactory(type='status_update',desc='blah blah blah',group=rg)
url = urlreverse('ietf.group.views.all_status')
response = self.client.get(url)
self.assertEqual(response.status_code,200)
def test_view_status_update_for_meeting(self):
chair = RoleFactory(name_id='chair',group__type_id='wg')
GroupEventFactory(type='status_update',group=chair.group)
sess = SessionFactory.create(meeting__type_id='ietf',group=chair.group,meeting__date=date_today()-datetime.timedelta(days=1))
url = urlreverse('ietf.group.views.group_about_status_meeting',kwargs={'acronym':chair.group.acronym,'num':sess.meeting.number})
response = self.client.get(url)
self.assertEqual(response.status_code,200)
url = urlreverse('ietf.group.views.group_about_status_meeting',kwargs={'group_type':chair.group.type_id,'acronym':chair.group.acronym,'num':sess.meeting.number})
response = self.client.get(url)
self.assertEqual(response.status_code,200)
class GroupParentLoopTests(TestCase):
def test_group_parent_loop(self):
mars = GroupFactory(acronym="mars",parent=Group.objects.get(acronym='farfut'))
test1 = Group.objects.create(
type_id="team",
acronym="testteam1",
name="Test One",
description="The test team 1 is testing.",
state_id="active",
parent = mars,
)
test2 = Group.objects.create(
type_id="team",
acronym="testteam2",
name="Test Two",
description="The test team 2 is testing.",
state_id="active",
parent = test1,
)
# Change the parent of Mars to make a loop
mars.parent = test2
# In face of the loop in the parent links, the code should not loop forever
import signal
def timeout_handler(signum, frame):
raise Exception("Infinite loop in parent links is not handled properly.")
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(1) # One second
try:
test2.is_descendant_of("ietf")
except AssertionError:
pass
except Exception:
raise
finally:
signal.alarm(0)
# If we get here, then there is not an infinite loop
return
class AcronymValidationTests(TestCase):
def test_admin_acronym_validation(self):
now = timezone.now()
form = AdminGroupForm({'acronym':'shouldpass','name':'should pass','type':'wg','state':'active','used_roles':'[]','time':now})
self.assertTrue(form.is_valid())
form = AdminGroupForm({'acronym':'should-fail','name':'should fail','type':'wg','state':'active','used_roles':'[]','time':now})
self.assertIn('acronym',form.errors)
form = AdminGroupForm({'acronym':'f','name':'should fail','type':'wg','state':'active','used_roles':'[]','time':now})
self.assertIn('acronym',form.errors)
# For the ITU we have a hierarchy of group names that use hyphens as delimiters
form = AdminGroupForm({'acronym':'should-pass','name':'should pass','type':'sdo','state':'active','used_roles':'[]','time':now})
self.assertTrue(form.is_valid())
form = AdminGroupForm({'acronym':'shouldfail-','name':'should fail','type':'wg','state':'active','used_roles':'[]','time':now})
self.assertIn('acronym',form.errors)
form = AdminGroupForm({'acronym':'shouldfail-','name':'should fail','type':'sdo','state':'active','used_roles':'[]','time':now})
self.assertIn('acronym',form.errors)
form = AdminGroupForm({'acronym':'-shouldfail','name':'should fail','type':'wg','state':'active','used_roles':'[]','time':now})
self.assertIn('acronym',form.errors)
form = AdminGroupForm({'acronym':'-shouldfail','name':'should fail','type':'sdo','state':'active','used_roles':'[]','time':now})
self.assertIn('acronym',form.errors)
# SDO groups (and only SDO groups) can have a leading number
form = AdminGroupForm({'acronym':'3gpp-should-pass','name':'should pass','type':'sdo','state':'active','used_roles':'[]','time':now})
self.assertTrue(form.is_valid())
form = AdminGroupForm({'acronym':'123shouldfail','name':'should fail','type':'wg','state':'active','used_roles':'[]','time':now})
self.assertIn('acronym',form.errors)
wg = GroupFactory(acronym='bad-idea', type_id='wg') # There are some existing wg and programs with hyphens in their acronyms.
form = AdminGroupForm({'acronym':wg.acronym,'name':wg.name,'type':wg.type_id,'state':wg.state_id,'used_roles':str(wg.used_roles),'time':now},instance=wg)
self.assertTrue(form.is_valid())
def test_groupform_acronym_validation(self):
form = GroupForm({'acronym':'shouldpass','name':'should pass','state':'active'},group_type='wg')
self.assertTrue(form.is_valid())
form = GroupForm({'acronym':'should-fail','name':'should fail','state':'active'},group_type='wg')
self.assertIn('acronym',form.errors)
form = GroupForm({'acronym':'f','name':'should fail','state':'active'},group_type='wg')
self.assertIn('acronym',form.errors)
form = GroupForm({'acronym':'should-pass','name':'should pass','state':'active'},group_type='sdo')
self.assertTrue(form.is_valid())
form = GroupForm({'acronym':'shouldfail-','name':'should fail','state':'active'},group_type='sdo')
self.assertIn('acronym',form.errors)
form = GroupForm({'acronym':'-shouldfail','name':'should fail','state':'active'},group_type='sdo')
self.assertIn('acronym',form.errors)
wg = GroupFactory(acronym='bad-idea', type_id='wg')
form = GroupForm({'acronym':wg.acronym,'name':wg.name,'state':wg.state_id},group=wg, group_type=wg.type_id)
self.assertTrue(form.is_valid())