diff --git a/bin/daily b/bin/daily
index b69e823b3..60738fd0a 100755
--- a/bin/daily
+++ b/bin/daily
@@ -61,4 +61,4 @@ $DTDIR/ietf/manage.py fetch_meeting_attendance --latest 2
$DTDIR/ietf/bin/send-review-reminders
# Purge old request_profiler records
-$DTDIR/ietf/manage.py purge_request_profiler_records
+$DTDIR/ietf/manage.py purge_request_profiler_records -d2
diff --git a/bin/dump-to-names-json b/bin/dump-to-names-json
new file mode 100644
index 000000000..9c7dfac07
--- /dev/null
+++ b/bin/dump-to-names-json
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+# This script provides a limited selected dump of database content with the
+# purpose of generating a test fixture that provides the test data needed
+# by the test suite.
+#
+# The generated data fixture is sorted and normalized in order to produce
+# minimal commit diffs which reflect only actual changes in the fixture data,
+# without apparent changes resulting only from ordering changes.
+
+set -x
+ietf/manage.py dumpdata --indent 1 doc.State doc.BallotType doc.StateType \
+ mailtrigger.MailTrigger mailtrigger.Recipient name utils.VersionInfo \
+ group.GroupFeatures stats.CountryAlias dbtemplate.DBTemplate \
+ | jq --sort-keys "sort_by(.model, .pk)" \
+ | jq '[.[] | select(.model!="dbtemplate.dbtemplate" or .pk==354)]' > ietf/name/fixtures/names.json
diff --git a/bin/every15m b/bin/every15m
index 0b1d3ab24..93e5ba670 100755
--- a/bin/every15m
+++ b/bin/every15m
@@ -16,7 +16,5 @@ source $DTDIR/env/bin/activate
logger -p user.info -t cron "Running $DTDIR/bin/every15m"
-
-
-
-
+# Send mail scheduled to go out at certain times
+$DTDIR/ietf/bin/send-scheduled-mail all
diff --git a/bin/test-crawl b/bin/test-crawl
index 6226d6837..a55ba3645 100755
--- a/bin/test-crawl
+++ b/bin/test-crawl
@@ -138,6 +138,7 @@ def check_html_valid(url, response, args):
key = re.sub("/dir/[a-z0-9-]+/", "/dir/foo/", key)
key = re.sub("/draft-[a-z0-9-]+/", "/draft-foo/", key)
key = re.sub("/group/[a-z0-9-]+/", "/group/foo/", key)
+ key = re.sub("/html/[a-z0-9-]+", "/html/foo/", key)
key = re.sub("/ipr/search/.*", "/ipr/search/", key)
key = re.sub("/meeting/[-0-9a-z]+/agenda/[0-9a-z]+/", "/meeting/nn/agenda/foo/", key)
key = re.sub("/release/[0-9dev.]+/", "/release/n.n.n/", key)
diff --git a/bin/weekly b/bin/weekly
index 8e01c273c..cca8403fd 100755
--- a/bin/weekly
+++ b/bin/weekly
@@ -20,3 +20,6 @@ logger -p user.info -t cron "Running $DTDIR/bin/weekly"
$DTDIR/ietf/manage.py send_apikey_usage_emails
+# Send notifications about coming expirations
+$DTDIR/ietf/bin/notify-expirations
+
diff --git a/buildbot/masters/datatracker/master.cfg b/buildbot/masters/datatracker/master.cfg
index dace7868d..ff5e5489e 100644
--- a/buildbot/masters/datatracker/master.cfg
+++ b/buildbot/masters/datatracker/master.cfg
@@ -20,19 +20,16 @@ c = BuildmasterConfig = {}
# slave name and password must be configured on the slave.
from buildbot.buildslave import BuildSlave
c['slaves'] = [
- BuildSlave("datatracker_lin_py27_1", datatracker_lin_py27_1_pw),
- BuildSlave("datatracker_lin_py27_2", datatracker_lin_py27_2_pw),
- BuildSlave("datatracker_lin_py27_3", datatracker_lin_py27_3_pw),
- BuildSlave("datatracker_osx_py27_4", datatracker_osx_py27_4_pw),
- BuildSlave("datatracker_lin_py27_5", datatracker_lin_py27_5_pw),
- BuildSlave("datatracker_lin_py27_6", datatracker_lin_py27_6_pw),
#
- BuildSlave("datatracker_lin_py36_1", datatracker_lin_py36_1_pw),
- BuildSlave("datatracker_lin_py36_2", datatracker_lin_py36_2_pw),
- BuildSlave("datatracker_lin_py36_3", datatracker_lin_py36_3_pw),
- BuildSlave("datatracker_lin_py36_4", datatracker_lin_py36_4_pw),
- BuildSlave("datatracker_lin_py36_5", datatracker_lin_py36_5_pw),
- BuildSlave("datatracker_lin_py36_6", datatracker_lin_py36_6_pw),
+ BuildSlave("dunkelfelder_lin_py36_1", dunkelfelder_lin_py36_1_pw),
+ BuildSlave("dunkelfelder_lin_py36_2", dunkelfelder_lin_py36_2_pw),
+ BuildSlave("dunkelfelder_lin_py36_3", dunkelfelder_lin_py36_3_pw),
+ BuildSlave("dunkelfelder_lin_py36_4", dunkelfelder_lin_py36_4_pw),
+
+ BuildSlave("dornfelder_lin_py36_1", dornfelder_lin_py36_1_pw),
+ BuildSlave("dornfelder_lin_py36_2", dornfelder_lin_py36_2_pw),
+ BuildSlave("dornfelder_lin_py36_3", dornfelder_lin_py36_3_pw),
+ BuildSlave("dornfelder_lin_py36_4", dornfelder_lin_py36_4_pw),
]
# 'protocols' contains information about protocols which master will use for
@@ -93,7 +90,7 @@ c['schedulers'] = [
# Periodic Schedulers
Nightly(name="lin_test_old_libs", hour=16, minute=12, branch="trunk", builderNames=["Verify Minimum Libs"],),
Nightly(name="lin_test_libs", hour=16, minute=42, branch="trunk", builderNames=["Verify Latest Libs"],),
- Nightly(name="crawler", hour=9, minute=00, branch="trunk", onlyIfChanged=True, builderNames=["Test-Crawler"],),
+ Nightly(name="crawler", hour=23, minute=00, branch="trunk", onlyIfChanged=True, builderNames=["Test-Crawler"],),
# Force schedulers
ForceScheduler(name="force_pyflakes", builderNames=["Check PyFlakes"]),
@@ -311,6 +308,7 @@ c['builders'] = []
# -*- section Builder_Run_pyflakes -*-
factory = BuildFactory()
+factory.addStep(SetPropertiesFromEnv(variables=['HOME',]))
factory.addStep(SVN(
username='buildbot@tools.ietf.org',
descriptionDone="svn update",
@@ -320,7 +318,13 @@ factory.addStep(SVN(
repourl=Interpolate('https://svn.tools.ietf.org/svn/tools/ietfdb/%(src::branch:~trunk)s'),
descriptionSuffix=[Interpolate('%(src::branch)s %(src::revision)s')],
))
-factory.addStep(SetPropertiesFromEnv(variables=['HOME',]))
+factory.addStep(ShellCommand(
+ descriptionDone="install requirements",
+ workdir=Interpolate('build/%(src::branch)s'),
+ haltOnFailure=True,
+ usePTY=False,
+ command=["pip", "install", "-r", "requirements.txt"],
+ ))
factory.addStep(ShellCommand(
descriptionDone="seting up settings_local.py",
workdir=Interpolate('build/%(src::branch)s'),
@@ -338,21 +342,23 @@ factory.addStep(PyFlakes(
factory.addStep(ShellCommand(
descriptionDone="mark as passed",
workdir=Interpolate('build/%(src::branch)s'),
+ flunkOnFailure=False,
usePTY=False,
command=["svn", "--username=buildbot@tools.ietf.org", "--non-interactive",
"propset", "--revprop", "-r", Property('got_revision'), "test:pyflakes", "passed" ],
))
c['builders'].append(BuilderConfig(name="Check PyFlakes", factory=factory, category="1. trunk",
- slavenames=["datatracker_lin_py36_1", "datatracker_lin_py36_4", ]))
+ slavenames=["dunkelfelder_lin_py36_1", "dornfelder_lin_py36_1", ]))
c['builders'].append(BuilderConfig(name="[branch] Check PyFlakes", factory=factory, category="2. branch",
- slavenames=["datatracker_lin_py36_2", ]))
+ slavenames=["dunkelfelder_lin_py36_2", "dornfelder_lin_py36_2", ]))
c['builders'].append(BuilderConfig(name="[personal] Check PyFlakes", factory=factory, category="3. personal",
- slavenames=["datatracker_lin_py36_3", ]))
+ slavenames=["dunkelfelder_lin_py36_2",]))
# -*- section Builder_TestSuite -*-
factory = BuildFactory()
+factory.addStep(SetPropertiesFromEnv(variables=['HOME',]))
factory.addStep(SVN(
username='buildbot@tools.ietf.org',
descriptionDone="svn update",
@@ -362,7 +368,7 @@ factory.addStep(SVN(
repourl=Interpolate('https://svn.tools.ietf.org/svn/tools/ietfdb/%(src::branch:~trunk)s'),
descriptionSuffix=[Interpolate('%(src::branch)s %(src::revision)s')],
))
-factory.addStep(RemovePYCs(workdir=Interpolate('build/%(src::branch)s')))
+factory.addStep(RemovePYCs(workdir=Interpolate('build/%(src::branch)s'), usePTY=False))
factory.addStep(ShellCommand(
descriptionDone="remove tmp-* dirs",
workdir=Interpolate('build/%(src::branch)s'),
@@ -377,7 +383,6 @@ factory.addStep(ShellCommand(
usePTY=False,
command=["pip", "install", "-r", "requirements.txt"],
))
-factory.addStep(SetPropertiesFromEnv(variables=['HOME',]))
factory.addStep(ShellCommand(
descriptionDone="copy settings_local.py",
workdir=Interpolate('build/%(src::branch)s'),
@@ -403,22 +408,32 @@ factory.addStep(UnitTest(
factory.addStep(ShellCommand(
descriptionDone="mark as passed",
workdir=Interpolate('build/%(src::branch)s'),
+ flunkOnFailure=False,
usePTY=False,
command=["svn", "--username=buildbot@tools.ietf.org", "--non-interactive",
"propset", "--revprop", "-r", Property('got_revision'), "test:unittest", "passed" ],
))
c['builders'].append(BuilderConfig(name="Test Suite", factory=factory, category="1. trunk",
- slavenames=["datatracker_lin_py36_1", "datatracker_lin_py36_4", ]))
+ slavenames=["dunkelfelder_lin_py36_1", "dornfelder_lin_py36_1", ]))
c['builders'].append(BuilderConfig(name="[branch] Test Suite", factory=factory, category="2. branch",
- slavenames=["datatracker_lin_py36_2", ]))
+ slavenames=["dunkelfelder_lin_py36_2", "dornfelder_lin_py36_2", ]))
c['builders'].append(BuilderConfig(name="[personal] Test Suite", factory=factory, category="3. personal",
- slavenames=["datatracker_lin_py36_3", ]))
+ slavenames=["dunkelfelder_lin_py36_2", "dornfelder_lin_py36_2", ]))
# -*- section Builder_TestCrawler -*-
factory = BuildFactory()
+factory.addStep(SetPropertiesFromEnv(variables=['HOME',]))
+factory.addStep(ShellCommand(
+ descriptionDone="update database",
+ workdir=Interpolate('build/%(src::branch)s'),
+ haltOnFailure=True,
+ usePTY=False,
+ timeout=3600, # 1 hour
+ command=["docker/updatedb", "-q"],
+ ))
factory.addStep(SVN(
username='buildbot@tools.ietf.org',
descriptionDone="svn update",
@@ -428,7 +443,7 @@ factory.addStep(SVN(
repourl=Interpolate('https://svn.tools.ietf.org/svn/tools/ietfdb/%(src::branch:~trunk)s'),
descriptionSuffix=[Interpolate('%(src::branch)s %(src::revision)s')],
))
-factory.addStep(RemovePYCs(workdir=Interpolate('build/%(src::branch)s')))
+factory.addStep(RemovePYCs(workdir=Interpolate('build/%(src::branch)s'), usePTY=False))
factory.addStep(ShellCommand(
descriptionDone="install requirements",
workdir=Interpolate('build/%(src::branch)s'),
@@ -436,7 +451,6 @@ factory.addStep(ShellCommand(
usePTY=False,
command=["pip", "install", "-r", "requirements.txt"],
))
-factory.addStep(SetPropertiesFromEnv(variables=['HOME',]))
factory.addStep(ShellCommand(
descriptionDone="copy settings_local.py",
workdir=Interpolate('build/%(src::branch)s'),
@@ -451,6 +465,14 @@ factory.addStep(ShellCommand(
usePTY=False,
command=["ietf/manage.py", "migrate"],
))
+# This will not only do a prelimnary sanity check, but also patch libs as needed:
+factory.addStep(ShellCommand(
+ descriptionDone="run django checks",
+ workdir=Interpolate('build/%(src::branch)s'),
+ haltOnFailure=True,
+ usePTY=False,
+ command=["ietf/manage.py", "check"],
+ ))
factory.addStep(TestCrawlerShellCommand(
workdir=Interpolate('build/%(src::branch)s'),
haltOnFailure=True,
@@ -461,13 +483,14 @@ factory.addStep(TestCrawlerShellCommand(
factory.addStep(ShellCommand(
descriptionDone="mark as passed",
workdir=Interpolate('build/%(src::branch)s'),
+ flunkOnFailure=False,
usePTY=False,
command=["svn", "--username=buildbot@tools.ietf.org", "--non-interactive",
"propset", "--revprop", "-r", Property('got_revision'), "test:crawler", "passed" ],
))
c['builders'].append(BuilderConfig(name="Test-Crawler", factory=factory, category="1. trunk",
- slavenames=["datatracker_lin_py36_6", ]))
+ slavenames=["dunkelfelder_lin_py36_4", ]))
# -*- section Builder_Verify_Old_Libs -*-
@@ -479,6 +502,7 @@ c['builders'].append(BuilderConfig(name="Test-Crawler", factory=factory, categor
# dependencies.
factory = BuildFactory()
+factory.addStep(SetPropertiesFromEnv(variables=['HOME',]))
factory.addStep(ShellCommand(
descriptionDone="remove tweaked requirements",
workdir=Interpolate('build/%(src::branch)s'),
@@ -497,7 +521,7 @@ factory.addStep(SVN(
repourl=Interpolate('https://svn.tools.ietf.org/svn/tools/ietfdb/%(src::branch:~trunk)s'),
descriptionSuffix=[Interpolate('%(src::branch)s %(src::revision)s')],
))
-factory.addStep(RemovePYCs(workdir=Interpolate('build/%(src::branch)s')))
+factory.addStep(RemovePYCs(workdir=Interpolate('build/%(src::branch)s'), usePTY=False))
factory.addStep(ShellCommand(
descriptionDone="edit requirements",
workdir=Interpolate('build/%(src::branch)s'),
@@ -512,7 +536,6 @@ factory.addStep(ShellCommand(
usePTY=False,
command=["pip", "install", "--upgrade", "-r", "requirements.txt"],
))
-factory.addStep(SetPropertiesFromEnv(variables=['HOME',]))
factory.addStep(ShellCommand(
descriptionDone="seting up settings_local.py",
workdir=Interpolate('build/%(src::branch)s'),
@@ -542,10 +565,10 @@ factory.addStep(UnitTest(
command=["ietf/manage.py", "test", "--settings=settings_sqlitetest", "--verbosity=2", ],
))
c['builders'].append(BuilderConfig(name="Verify Minimum Libs", factory=factory, category="1. trunk",
- slavenames=["datatracker_lin_py36_5", ]))
+ slavenames=["dornfelder_lin_py36_3", ]))
-# -*- section Builder_Dependencies -*-
+# -*- section Verify_Latest_Libs -*-
# This build runs pip install --upgrade, to make sure that we install the latest version of all
# dependencies, in order to get an indication if/when an incompatibility turns up with a new
@@ -554,6 +577,7 @@ c['builders'].append(BuilderConfig(name="Verify Minimum Libs", factory=factory,
# dependencies.
factory = BuildFactory()
+factory.addStep(SetPropertiesFromEnv(variables=['HOME',]))
factory.addStep(SVN(
username='buildbot@tools.ietf.org',
descriptionDone="svn update",
@@ -564,7 +588,7 @@ factory.addStep(SVN(
repourl=Interpolate('https://svn.tools.ietf.org/svn/tools/ietfdb/%(src::branch:~trunk)s'),
descriptionSuffix=[Interpolate('%(src::branch)s %(src::revision)s')],
))
-factory.addStep(RemovePYCs(workdir=Interpolate('build/%(src::branch)s')))
+factory.addStep(RemovePYCs(workdir=Interpolate('build/%(src::branch)s'), usePTY=False))
factory.addStep(ShellCommand(
descriptionDone="install/upgrade requirements",
workdir=Interpolate('build/%(src::branch)s'),
@@ -572,7 +596,6 @@ factory.addStep(ShellCommand(
usePTY=False,
command=["pip", "install", "--upgrade", "-r", "requirements.txt"],
))
-factory.addStep(SetPropertiesFromEnv(variables=['HOME',]))
factory.addStep(ShellCommand(
descriptionDone="seting up settings_local.py",
workdir=Interpolate('build/%(src::branch)s'),
@@ -603,7 +626,7 @@ factory.addStep(UnitTest(
))
c['builders'].append(BuilderConfig(name="Verify Latest Libs", factory=factory, category="1. trunk",
- slavenames=["datatracker_lin_py36_5", ]))
+ slavenames=["dornfelder_lin_py36_3", ]))
####### STATUS TARGETS
@@ -673,7 +696,7 @@ c['status'].append(mail.MailNotifier(
# installation's html.WebStatus home page (linked to the
# 'titleURL') and is embedded in the title of the waterfall HTML page.
-c['title'] = "IETF Datatracker"
+c['title'] = "Buildbot: IETF Datatracker"
c['titleURL'] = "https://datatracker.ietf.org/"
# the 'buildbotURL' string should point to the location where the buildbot's
diff --git a/ietf/doc/models.py b/ietf/doc/models.py
index 4ac004661..dc92d356d 100644
--- a/ietf/doc/models.py
+++ b/ietf/doc/models.py
@@ -158,7 +158,7 @@ class DocumentInfo(models.Model):
else:
self._cached_base_name = "%s-%s.txt" % (self.name, self.rev)
elif self.type_id in ["slides", "agenda", "minutes", "bluesheets", ] and self.meeting_related():
- self._cached_base_name = "%s-%s.txt" % self.canonical_name()
+ self._cached_base_name = "%s-%s.txt" % (self.canonical_name(), self.rev)
elif self.type_id == 'review':
# TODO: This will be wrong if a review is updated on the same day it was created (or updated more than once on the same day)
self._cached_base_name = "%s.txt" % self.name
diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py
index f514de2ba..9a13459b7 100644
--- a/ietf/doc/views_doc.py
+++ b/ietf/doc/views_doc.py
@@ -256,7 +256,9 @@ def document_main(request, name, rev=None):
if "pdf" not in found_types:
file_urls.append(("pdf", settings.TOOLS_ID_PDF_URL + doc.name + "-" + doc.rev + ".pdf"))
- file_urls.append(("htmlized", settings.TOOLS_ID_HTML_URL + doc.name + "-" + doc.rev))
+ #file_urls.append(("htmlized", settings.TOOLS_ID_HTML_URL + doc.name + "-" + doc.rev))
+ file_urls.append(("htmlized (tools)", settings.TOOLS_ID_HTML_URL + doc.name + "-" + doc.rev))
+ file_urls.append(("htmlized", urlreverse('ietf.doc.views_doc.document_html', kwargs=dict(name=doc.name, rev=doc.rev))))
# latest revision
latest_revision = doc.latest_event(NewRevisionDocEvent, type="new_revision")
@@ -683,7 +685,7 @@ def document_html(request, name, rev=None):
doc = docs.get()
if not os.path.exists(doc.get_file_name()):
- raise Http404("Document not found: %s" % doc.get_base_name())
+ raise Http404("File not found: %s" % doc.get_file_name())
top = render_document_top(request, doc, "status", name)
if not rev and not name.startswith('rfc'):
diff --git a/ietf/group/factories.py b/ietf/group/factories.py
index 64468ece8..77b937044 100644
--- a/ietf/group/factories.py
+++ b/ietf/group/factories.py
@@ -18,6 +18,15 @@ class GroupFactory(factory.DjangoModelFactory):
list_email = factory.LazyAttribute(lambda a: '%s@ietf.org'% a.acronym)
uses_milestone_dates = True
+ @factory.lazy_attribute
+ def parent(self):
+ if self.type_id in ['wg','ag']:
+ return GroupFactory(type_id='area')
+ elif self.type_id in ['rg']:
+ return GroupFactory(acronym='irtf', type_id='irtf')
+ else:
+ return None
+
class ReviewTeamFactory(GroupFactory):
type_id = 'review'
diff --git a/ietf/group/migrations/0024_add_groupman_authroles.py b/ietf/group/migrations/0024_add_groupman_authroles.py
new file mode 100644
index 000000000..5c6461805
--- /dev/null
+++ b/ietf/group/migrations/0024_add_groupman_authroles.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.29 on 2020-05-04 13:10
+from __future__ import unicode_literals
+
+from django.db import migrations
+import jsonfield.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('group', '0023_use_milestone_dates_default_to_true'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='groupfeatures',
+ name='groupman_authroles',
+ field=jsonfield.fields.JSONField(default=['Secretariat'], max_length=128),
+ ),
+ migrations.AddField(
+ model_name='historicalgroupfeatures',
+ name='groupman_authroles',
+ field=jsonfield.fields.JSONField(default=['Secretariat'], max_length=128),
+ ),
+ ]
diff --git a/ietf/group/migrations/0025_populate_groupman_authroles.py b/ietf/group/migrations/0025_populate_groupman_authroles.py
new file mode 100644
index 000000000..d024c8bd2
--- /dev/null
+++ b/ietf/group/migrations/0025_populate_groupman_authroles.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.29 on 2020-05-01 12:54
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+authroles_map = {
+ 'adhoc': ['Secretariat'],
+ 'admin': ['Secretariat'],
+ 'ag': ['Secretariat', 'Area Director'],
+ 'area': ['Secretariat'],
+ 'dir': ['Secretariat'],
+ 'iab': ['Secretariat'],
+ 'iana': ['Secretariat'],
+ 'iesg': ['Secretariat'],
+ 'ietf': ['Secretariat'],
+ 'individ': [],
+ 'irtf': ['Secretariat'],
+ 'ise': ['Secretariat'],
+ 'isoc': ['Secretariat'],
+ 'nomcom': ['Secretariat'],
+ 'program': ['Secretariat', 'IAB'],
+ 'review': ['Secretariat'],
+ 'rfcedtyp': ['Secretariat'],
+ 'rg': ['Secretariat', 'IRTF Chair'],
+ 'sdo': ['Secretariat'],
+ 'team': ['Secretariat'],
+ 'wg': ['Secretariat', 'Area Director'],
+}
+
+def forward(apps, schema_editor):
+ GroupFeatures = apps.get_model('group', 'GroupFeatures')
+ for type_id, authroles in authroles_map.items():
+ GroupFeatures.objects.filter(type_id=type_id).update(groupman_authroles=authroles)
+
+def reverse(apps, schema_editor):
+ pass
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('group', '0024_add_groupman_authroles'),
+ ]
+
+ operations = [
+ migrations.RunPython(forward, reverse),
+ ]
diff --git a/ietf/group/migrations/0026_programs_meet.py b/ietf/group/migrations/0026_programs_meet.py
new file mode 100644
index 000000000..4fb6fda06
--- /dev/null
+++ b/ietf/group/migrations/0026_programs_meet.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.29 on 2020-05-01 12:54
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+def forward(apps, schema_editor):
+ GroupFeatures = apps.get_model('group', 'GroupFeatures')
+ program = GroupFeatures.objects.get(type_id='program')
+ program.has_meetings = True
+ program.matman_roles = ['lead', 'chair', 'secr']
+ program.docman_roles = ['lead', 'chair', 'secr']
+ program.groupman_roles = ['lead', 'chair', 'secr']
+ program.role_order = ['lead', 'chair', 'secr']
+ program.save()
+
+def reverse(apps, schema_editor):
+ GroupFeatures = apps.get_model('group', 'GroupFeatures')
+ program = GroupFeatures.objects.get(type_id='program')
+ program.has_meetings = False
+ program.matman_roles = ['lead', 'secr']
+ program.docman_roles = ['lead', 'secr']
+ program.groupman_roles = ['lead', 'secr']
+ program.role_order = ['lead', 'secr']
+ program.save()
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('group', '0025_populate_groupman_authroles'),
+ ]
+
+ operations = [
+ migrations.RunPython(forward, reverse),
+ ]
diff --git a/ietf/group/migrations/0027_programs_have_parents.py b/ietf/group/migrations/0027_programs_have_parents.py
new file mode 100644
index 000000000..d05d02009
--- /dev/null
+++ b/ietf/group/migrations/0027_programs_have_parents.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.29 on 2020-05-08 09:02
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+def forward(apps, schema_editor):
+ Group = apps.get_model('group','Group')
+ iab = Group.objects.get(acronym='iab')
+ Group.objects.filter(type_id='program').update(parent=iab)
+
+def reverse(apps, schema_editor):
+ pass # No point in removing the parents
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('group', '0026_programs_meet'),
+ ]
+
+ operations = [
+ migrations.RunPython(forward, reverse),
+ ]
diff --git a/ietf/group/models.py b/ietf/group/models.py
index 80258f2f2..5710af219 100644
--- a/ietf/group/models.py
+++ b/ietf/group/models.py
@@ -65,9 +65,6 @@ class GroupInfo(models.Model):
kwargs["group_type"] = self.type_id
return urlreverse(self.features.about_page, kwargs=kwargs)
- def interim_approval_roles(self):
- return list(set([ role for role in self.parent.role_set.filter(name__in=['ad', 'chair']) ]))
-
def is_bof(self):
return self.state_id in ["bof", "bof-conc"]
@@ -238,6 +235,7 @@ class GroupFeatures(models.Model):
admin_roles = jsonfield.JSONField(max_length=64, blank=False, default=["chair"]) # Trac Admin
docman_roles = jsonfield.JSONField(max_length=128, blank=False, default=["ad","chair","delegate","secr"])
groupman_roles = jsonfield.JSONField(max_length=128, blank=False, default=["ad","chair",])
+ groupman_authroles = jsonfield.JSONField(max_length=128, blank=False, default=["Secretariat",])
matman_roles = jsonfield.JSONField(max_length=128, blank=False, default=["ad","chair","delegate","secr"])
role_order = jsonfield.JSONField(max_length=128, blank=False, default=["chair","secr","member"],
help_text="The order in which roles are shown, for instance on photo pages. Enter valid JSON.")
diff --git a/ietf/group/tests_info.py b/ietf/group/tests_info.py
index 478451e2c..8a748086d 100644
--- a/ietf/group/tests_info.py
+++ b/ietf/group/tests_info.py
@@ -264,7 +264,7 @@ class GroupPagesTests(TestCase):
can_edit = {
'wg' : ['secretary','ad'],
'rg' : ['secretary','irtf-chair'],
- 'ag' : ['secretary', ],
+ 'ag' : ['secretary', 'ad' ],
'team' : ['secretary',], # The code currently doesn't let ads edit teams or directorates. Maybe it should.
'dir' : ['secretary',],
'review' : ['secretary',],
diff --git a/ietf/group/utils.py b/ietf/group/utils.py
index 65be4861f..6272d79e7 100644
--- a/ietf/group/utils.py
+++ b/ietf/group/utils.py
@@ -15,7 +15,7 @@ import debug # pyflakes:ignore
from ietf.community.models import CommunityList, SearchRule
from ietf.community.utils import reset_name_contains_index_for_rule, can_manage_community_list
from ietf.doc.models import Document, State
-from ietf.group.models import Group, RoleHistory, Role
+from ietf.group.models import Group, RoleHistory, Role, GroupFeatures
from ietf.ietfauth.utils import has_role
from ietf.name.models import GroupTypeName
from ietf.person.models import Email
@@ -105,6 +105,7 @@ def save_milestone_in_history(milestone):
return h
+# TODO: rework this using features.groupman_authroles
def can_manage_group_type(user, group, type_id=None):
if not user.is_authenticated:
return False
@@ -125,8 +126,11 @@ def can_manage_group_type(user, group, type_id=None):
return has_role(user, ('Secretariat'))
def can_manage_group(user, group):
- if can_manage_group_type(user, group):
- return True
+ if not user.is_authenticated:
+ return False
+ for authrole in group.features.groupman_authroles:
+ if has_role(user, authrole):
+ return True
return group.has_role(user, group.features.groupman_roles)
def milestone_reviewer_for_group_type(group_type):
@@ -141,6 +145,18 @@ def can_manage_materials(user, group):
def can_manage_session_materials(user, group, session):
return has_role(user, 'Secretariat') or (group.has_role(user, group.features.matman_roles) and not session.is_material_submission_cutoff())
+# Maybe this should be cached...
+def can_manage_some_groups(user):
+ if not user.is_authenticated:
+ return False
+ for gf in GroupFeatures.objects.all():
+ for authrole in gf.groupman_authroles:
+ if has_role(user, authrole):
+ return True
+ if Role.objects.filter(name__in=gf.groupman_roles, group__type_id=gf.type_id, person__user=user).exists():
+ return True
+ return False
+
def can_provide_status_update(user, group):
if not group.features.acts_like_wg:
return False
diff --git a/ietf/ietfauth/utils.py b/ietf/ietfauth/utils.py
index 34d410980..b0cd1b640 100644
--- a/ietf/ietfauth/utils.py
+++ b/ietf/ietfauth/utils.py
@@ -70,6 +70,9 @@ def has_role(user, role_names, *args, **kwargs):
"RG Secretary": Q(person=person,name="secr", group__type="rg", group__state__in=["active","proposed"]),
"AG Secretary": Q(person=person,name="secr", group__type="ag", group__state__in=["active"]),
"Team Chair": Q(person=person,name="chair", group__type="team", group__state="active"),
+ "Program Lead": Q(person=person,name="lead", group__type="program", group__state="active"),
+ "Program Secretary": Q(person=person,name="secr", group__type="program", group__state="active"),
+ "Program Chair": Q(person=person,name="chair", group__type="program", group__state="active"),
"Nomcom Chair": Q(person=person, name="chair", group__type="nomcom", group__acronym__icontains=kwargs.get('year', '0000')),
"Nomcom Advisor": Q(person=person, name="advisor", group__type="nomcom", group__acronym__icontains=kwargs.get('year', '0000')),
"Nomcom": Q(person=person, group__type="nomcom", group__acronym__icontains=kwargs.get('year', '0000')),
diff --git a/ietf/mailtrigger/models.py b/ietf/mailtrigger/models.py
index 92e5b1dda..b971ed991 100644
--- a/ietf/mailtrigger/models.py
+++ b/ietf/mailtrigger/models.py
@@ -166,6 +166,8 @@ class Recipient(models.Model):
addrs.extend(group.role_set.filter(name='ad').values_list('email__address',flat=True))
if group.type_id=='rg':
addrs.extend(Recipient.objects.get(slug='stream_managers').gather(**{'streams':['irtf']}))
+ elif group.type_id=='program':
+ addrs.extend(Recipient.objects.get(slug='iab').gather(**{}))
return addrs
def gather_group_secretaries(self, **kwargs):
diff --git a/ietf/meeting/forms.py b/ietf/meeting/forms.py
index c0e4475c4..9818f7b2a 100644
--- a/ietf/meeting/forms.py
+++ b/ietf/meeting/forms.py
@@ -15,7 +15,7 @@ from django.forms import BaseInlineFormSet
import debug # pyflakes:ignore
from ietf.doc.models import Document, DocAlias, State, NewRevisionDocEvent
-from ietf.group.models import Group
+from ietf.group.models import Group, GroupFeatures
from ietf.ietfauth.utils import has_role
from ietf.meeting.models import Session, Meeting, Schedule, countries, timezones
from ietf.meeting.helpers import get_next_interim_number, make_materials_directories
@@ -100,8 +100,7 @@ class InterimSessionInlineFormSet(BaseInlineFormSet):
return # formset doesn't have cleaned_data
class InterimMeetingModelForm(forms.ModelForm):
- # TODO: Should area groups get to schedule Interims?
- group = GroupModelChoiceField(queryset=Group.objects.filter(type__in=('wg', 'rg', 'ag'), state__in=('active', 'proposed', 'bof')).order_by('acronym'), required=False)
+ group = GroupModelChoiceField(queryset=Group.objects.filter(type_id__in=GroupFeatures.objects.filter(has_meetings=True).values_list('type_id',flat=True), state__in=('active', 'proposed', 'bof')).order_by('acronym'), required=False)
in_person = forms.BooleanField(required=False)
meeting_type = forms.ChoiceField(choices=(
("single", "Single"),
@@ -156,13 +155,15 @@ class InterimMeetingModelForm(forms.ModelForm):
return # don't reduce group options
q_objects = Q()
if has_role(self.user, "Area Director"):
- q_objects.add(Q(type="wg", state__in=("active", "proposed", "bof")), Q.OR)
+ q_objects.add(Q(type__in=["wg", "ag"], state__in=("active", "proposed", "bof")), Q.OR)
if has_role(self.user, "IRTF Chair"):
q_objects.add(Q(type="rg", state__in=("active", "proposed")), Q.OR)
if has_role(self.user, "WG Chair"):
q_objects.add(Q(type="wg", state__in=("active", "proposed", "bof"), role__person=self.person, role__name="chair"), Q.OR)
if has_role(self.user, "RG Chair"):
q_objects.add(Q(type="rg", state__in=("active", "proposed"), role__person=self.person, role__name="chair"), Q.OR)
+ if has_role(self.user, "Program Lead") or has_role(self.user, "Program Chair"):
+ q_objects.add(Q(type="program", state__in=("active", "proposed"), role__person=self.person, role__name__in=["chair", "lead"]), Q.OR)
queryset = Group.objects.filter(q_objects).distinct().order_by('acronym')
self.fields['group'].queryset = queryset
diff --git a/ietf/meeting/helpers.py b/ietf/meeting/helpers.py
index 47522fce3..5d6aacaee 100644
--- a/ietf/meeting/helpers.py
+++ b/ietf/meeting/helpers.py
@@ -21,6 +21,7 @@ import debug # pyflakes:ignore
from ietf.doc.models import Document
from ietf.group.models import Group
+from ietf.group.utils import can_manage_some_groups, can_manage_group
from ietf.ietfauth.utils import has_role, user_is_person
from ietf.liaisons.utils import get_person_for_user
from ietf.mailtrigger.utils import gather_address_lists
@@ -324,11 +325,14 @@ def can_approve_interim_request(meeting, user):
if not session:
return False
group = session.group
- if group.type.slug == 'wg':
+ if group.type.slug in ['wg','ag']:
if group.parent.role_set.filter(name='ad', person=person) or group.role_set.filter(name='ad', person=person):
return True
if group.type.slug == 'rg' and group.parent.role_set.filter(name='chair', person=person):
return True
+ if group.type.slug == 'program':
+ if person.role_set.filter(group__acronym='iab', name='member'):
+ return True
return False
@@ -336,14 +340,13 @@ def can_edit_interim_request(meeting, user):
'''Returns True if the user can edit the interim meeting request'''
if meeting.type.slug != 'interim':
return False
- if has_role(user, 'Secretariat'):
+ if has_role(user, 'Secretariat'): # Consider removing - can_manage_group should handle this
return True
- person = get_person_for_user(user)
session = meeting.session_set.first()
if not session:
return False
group = session.group
- if group.role_set.filter(name='chair', person=person):
+ if can_manage_group(user, group):
return True
elif can_approve_interim_request(meeting, user):
return True
@@ -352,29 +355,17 @@ def can_edit_interim_request(meeting, user):
def can_request_interim_meeting(user):
- if has_role(user, ('Secretariat', 'Area Director', 'WG Chair', 'IRTF Chair', 'RG Chair')):
- return True
- return False
-
+ return can_manage_some_groups(user)
def can_view_interim_request(meeting, user):
'''Returns True if the user can see the pending interim request in the pending interim view'''
if meeting.type.slug != 'interim':
return False
- if has_role(user, 'Secretariat'):
- return True
- person = get_person_for_user(user)
session = meeting.session_set.first()
if not session:
return False
group = session.group
- if has_role(user, 'Area Director') and group.type.slug == 'wg':
- return True
- if has_role(user, 'IRTF Chair') and group.type.slug == 'rg':
- return True
- if group.role_set.filter(name='chair', person=person):
- return True
- return False
+ return can_manage_group(user, group)
def create_interim_meeting(group, date, city='', country='', timezone='UTC',
@@ -512,11 +503,17 @@ def send_interim_approval_request(meetings):
else:
is_series = False
approver_set = set()
- for role in group.interim_approval_roles():
- approver = "%s of the %s" % ( role.name.name, role.group.name)
- approver_set.add(approver)
+ for authrole in group.features.groupman_authroles: # NOTE: This makes an assumption that the authroles are exactly the set of approvers
+ approver_set.add(authrole)
approvers = list(approver_set)
- context = locals() # TODO Unnecessarily complex, context needs to only contain what the template needs
+ context = {
+ 'group': group,
+ 'is_series': is_series,
+ 'meetings': meetings,
+ 'approvers': approvers,
+ 'requester': requester,
+ 'approval_urls': approval_urls,
+ }
send_mail(None,
to_email,
from_email,
diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py
index 13db30b92..69c50f6a9 100644
--- a/ietf/meeting/models.py
+++ b/ietf/meeting/models.py
@@ -793,7 +793,7 @@ class SchedTimeSessAssignment(models.Model):
if not self.timeslot:
components.append("unknown")
- if not self.session or not (getattr(self.session, "historic_group") or self.session.group):
+ if not self.session or not (getattr(self.session, "historic_group", None) or self.session.group):
components.append("unknown")
else:
components.append(self.timeslot.time.strftime("%Y-%m-%d-%a-%H%M"))
diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py
index 50f980419..7f90e295f 100644
--- a/ietf/meeting/tests_views.py
+++ b/ietf/meeting/tests_views.py
@@ -26,7 +26,8 @@ from django.db.models import F
import debug # pyflakes:ignore
from ietf.doc.models import Document
-from ietf.group.models import Group, Role
+from ietf.group.models import Group, Role, GroupFeatures
+from ietf.group.utils import can_manage_group
from ietf.person.models import Person
from ietf.meeting.helpers import can_approve_interim_request, can_view_interim_request
from ietf.meeting.helpers import send_interim_approval_request
@@ -37,7 +38,7 @@ from ietf.meeting.test_data import make_meeting_test_data, make_interim_meeting
from ietf.meeting.utils import finalize, condition_slide_order
from ietf.meeting.utils import add_event_info_to_session_qs
from ietf.meeting.views import session_draft_list
-from ietf.name.models import SessionStatusName, ImportantDateName
+from ietf.name.models import SessionStatusName, ImportantDateName, RoleName
from ietf.utils.decorators import skip_coverage
from ietf.utils.mail import outbox, empty_outbox, get_payload
from ietf.utils.test_utils import TestCase, login_testing_unauthorized, unicontent
@@ -1242,6 +1243,12 @@ class SessionDetailsTests(TestCase):
self.assertTrue(all([x in unicontent(r) for x in ('slides','agenda','minutes','draft')]))
self.assertNotContains(r, 'deleted')
+ q = PyQuery(r.content)
+ self.assertTrue(q('h2#session_%s div#session-buttons-%s' % (session.id, session.id)),
+ 'Session detail page does not contain session tool buttons')
+ self.assertFalse(q('h2#session_%s div#session-buttons-%s span.fa-arrows-alt' % (session.id, session.id)),
+ 'The session detail page is incorrectly showing the "Show meeting materials" button')
+
def test_session_details_past_interim(self):
group = GroupFactory.create(type_id='wg',state_id='active')
chair = RoleFactory(name_id='chair',group=group)
@@ -1491,8 +1498,7 @@ class InterimTests(TestCase):
self.assertContains(r, 'IETF 72')
# cancelled session
q = PyQuery(r.content)
-# self.assertIn('CANCELLED', q('[id*="-ames"]').text())
- self.assertIn('CANCELLED', q('tr>td>a>span').text())
+ self.assertIn('CANCELLED', q('tr>td.text-right>span').text())
self.check_interim_tabs(url)
def test_upcoming_ical(self):
@@ -1555,7 +1561,8 @@ class InterimTests(TestCase):
r = self.client.get("/meeting/interim/request/")
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
- self.assertEqual(Group.objects.filter(type__in=('wg', 'rg', 'ag'), state__in=('active', 'proposed')).count(),
+ Group.objects.filter(type_id__in=GroupFeatures.objects.filter(has_meetings=True).values_list('type_id',flat=True), state__in=('active', 'proposed', 'bof'))
+ self.assertEqual(Group.objects.filter(type_id__in=GroupFeatures.objects.filter(has_meetings=True).values_list('type_id',flat=True), state__in=('active', 'proposed', 'bof')).count(),
len(q("#id_group option")) - 1) # -1 for options placeholder
self.client.logout()
@@ -1939,6 +1946,28 @@ class InterimTests(TestCase):
user = User.objects.get(username='ameschairman')
self.assertFalse(can_view_interim_request(meeting=meeting,user=user))
+ def test_can_manage_group(self):
+ make_meeting_test_data()
+ # unprivileged user
+ user = User.objects.get(username='plain')
+ group = Group.objects.get(acronym='mars')
+ self.assertFalse(can_manage_group(user=user,group=group))
+ # Secretariat
+ user = User.objects.get(username='secretary')
+ self.assertTrue(can_manage_group(user=user,group=group))
+ # related AD
+ user = User.objects.get(username='ad')
+ self.assertTrue(can_manage_group(user=user,group=group))
+ # other AD
+ user = User.objects.get(username='ops-ad')
+ self.assertTrue(can_manage_group(user=user,group=group))
+ # WG Chair
+ user = User.objects.get(username='marschairman')
+ self.assertTrue(can_manage_group(user=user,group=group))
+ # Other WG Chair
+ user = User.objects.get(username='ameschairman')
+ self.assertFalse(can_manage_group(user=user,group=group))
+
def test_interim_request_details(self):
make_meeting_test_data()
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting
@@ -1983,13 +2012,6 @@ class InterimTests(TestCase):
make_meeting_test_data()
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting
url = urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number})
- # ensure no cancel button for unauthorized user
- self.client.login(username="ameschairman", password="ameschairman+password")
- r = self.client.get(url)
- self.assertEqual(r.status_code, 200)
- q = PyQuery(r.content)
- self.assertEqual(len(q("a.btn:contains('Cancel')")), 0)
- # ensure cancel button for authorized user
self.client.login(username="marschairman", password="marschairman+password")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
@@ -2793,3 +2815,175 @@ class SessionTests(TestCase):
})
self.assertEqual(r.status_code,302)
self.assertEqual(len(outbox),1)
+
+class HasMeetingsTests(TestCase):
+ def setUp(self):
+ self.materials_dir = self.tempdir('materials')
+ #
+ self.saved_agenda_path = settings.AGENDA_PATH
+ #
+ settings.AGENDA_PATH = self.materials_dir
+
+ def tearDown(self):
+ shutil.rmtree(self.materials_dir)
+ #
+ settings.AGENDA_PATH = self.saved_agenda_path
+
+ def do_request_interim(self, url, group, user, meeting_count):
+ login_testing_unauthorized(self,user.username, url)
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ self.assertTrue(q('#id_group option[value="%d"]'%group.pk))
+ date = datetime.date.today() + datetime.timedelta(days=30+meeting_count)
+ time = datetime.datetime.now().time().replace(microsecond=0,second=0)
+ remote_instructions = 'Use webex'
+ agenda = 'Intro. Slides. Discuss.'
+ agenda_note = 'On second level'
+ meeting_count = Meeting.objects.filter(number__contains='-%s-'%group.acronym, date__year=date.year).count()
+ next_num = "%02d" % (meeting_count+1)
+ data = {'group':group.pk,
+ 'meeting_type':'single',
+ 'city':'',
+ 'country':'',
+ 'time_zone':'UTC',
+ 'session_set-0-date':date.strftime("%Y-%m-%d"),
+ 'session_set-0-time':time.strftime('%H:%M'),
+ 'session_set-0-requested_duration':'03:00:00',
+ 'session_set-0-remote_instructions':remote_instructions,
+ 'session_set-0-agenda':agenda,
+ 'session_set-0-agenda_note':agenda_note,
+ 'session_set-TOTAL_FORMS':1,
+ 'session_set-INITIAL_FORMS':0,
+ 'session_set-MIN_NUM_FORMS':0,
+ 'session_set-MAX_NUM_FORMS':1000}
+
+ r = self.client.post(urlreverse("ietf.meeting.views.interim_request"),data)
+ self.assertRedirects(r,urlreverse('ietf.meeting.views.upcoming'))
+ meeting = Meeting.objects.order_by('id').last()
+ self.assertEqual(meeting.type_id,'interim')
+ self.assertEqual(meeting.date,date)
+ self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date.year, group.acronym, next_num))
+ self.client.logout()
+
+
+ def create_role_for_authrole(self, authrole):
+ role = None
+ if authrole == 'Secretariat':
+ role = RoleFactory.create(group__acronym='secretariat',name_id='secr')
+ elif authrole == 'Area Director':
+ role = RoleFactory.create(name_id='ad', group__type_id='area')
+ elif authrole == 'IAB':
+ role = RoleFactory.create(name_id='member', group__acronym='iab')
+ elif authrole == 'IRTF Chair':
+ role = RoleFactory.create(name_id='chair', group__acronym='irtf')
+ if role is None:
+ self.assertIsNone("Can't test authrole:"+authrole)
+ self.assertNotEqual(role, None)
+ return role
+
+
+ def test_can_request_interim(self):
+
+ url = urlreverse('ietf.meeting.views.interim_request')
+ for gf in GroupFeatures.objects.filter(has_meetings=True):
+ meeting_count = 0
+ for role in gf.groupman_roles:
+ role = RoleFactory(group__type_id=gf.type_id, name_id=role)
+ self.do_request_interim(url, role.group, role.person.user, meeting_count)
+ for authrole in gf.groupman_authroles:
+ group = GroupFactory(type_id=gf.type_id)
+ role = self.create_role_for_authrole(authrole)
+ self.do_request_interim(url, group, role.person.user, 0)
+
+
+ def test_cannot_request_interim(self):
+
+ url = urlreverse('ietf.meeting.views.interim_request')
+
+ self.client.login(username='secretary', password='secretary+password')
+ nomeetings = []
+ for gf in GroupFeatures.objects.exclude(has_meetings=True):
+ nomeetings.append(GroupFactory(type_id=gf.type_id))
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ for group in nomeetings:
+ self.assertFalse(q('#id_group option[value="%d"]'%group.pk))
+ self.client.logout()
+
+ all_role_names = set(RoleName.objects.values_list('slug',flat=True))
+ for gf in GroupFeatures.objects.filter(has_meetings=True):
+ for role_name in all_role_names - set(gf.groupman_roles):
+ role = RoleFactory(group__type_id=gf.type_id,name_id=role_name)
+ self.client.login(username=role.person.user.username, password=role.person.user.username+'+password')
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 403)
+ self.client.logout()
+
+ def test_appears_on_upcoming(self):
+ url = urlreverse('ietf.meeting.views.upcoming')
+ for gf in GroupFeatures.objects.filter(has_meetings=True):
+ session = SessionFactory(
+ group__type_id = gf.type_id,
+ meeting__type_id='interim',
+ meeting__date = datetime.datetime.today()+datetime.timedelta(days=30),
+ status_id='sched',
+ )
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ self.assertIn(session.meeting.number, q('.interim-meeting-link').text())
+
+
+ def test_appears_on_pending(self):
+ url = urlreverse('ietf.meeting.views.interim_pending')
+ for gf in GroupFeatures.objects.filter(has_meetings=True):
+ group = GroupFactory(type_id=gf.type_id)
+ meeting_date = datetime.datetime.today() + datetime.timedelta(days=30)
+ session = SessionFactory(
+ group=group,
+ meeting__type_id='interim',
+ meeting__date = meeting_date,
+ meeting__number = 'interim-%d-%s-00'%(meeting_date.year,group.acronym),
+ status_id='apprw',
+ )
+ for role_name in gf.groupman_roles:
+ role = RoleFactory(group=group, name_id=role_name)
+ self.client.login(username=role.person.user.username, password=role.person.user.username+'+password')
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ self.assertIn(session.meeting.number, q('.interim-meeting-link').text())
+ self.client.logout()
+ for authrole in gf.groupman_authroles:
+ role = self.create_role_for_authrole(authrole)
+ self.client.login(username=role.person.user.username, password=role.person.user.username+'+password')
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ self.assertIn(session.meeting.number, q('.interim-meeting-link').text())
+ self.client.logout()
+
+
+ def test_appears_on_announce(self):
+ url = urlreverse('ietf.meeting.views.interim_announce')
+ login_testing_unauthorized(self,"secretary",url)
+ sessions=[]
+ for gf in GroupFeatures.objects.filter(has_meetings=True):
+ group = GroupFactory(type_id=gf.type_id)
+ meeting_date = datetime.datetime.today() + datetime.timedelta(days=30)
+ session = SessionFactory(
+ group=group,
+ meeting__type_id='interim',
+ meeting__date = meeting_date,
+ meeting__number = 'interim-%d-%s-00'%(meeting_date.year,group.acronym),
+ status_id='scheda',
+ )
+ sessions.append(session)
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ for session in sessions:
+ self.assertIn(session.meeting.number, q('.interim-meeting-link').text())
+
diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py
index 61b5eafd1..fe229b6fa 100644
--- a/ietf/meeting/views.py
+++ b/ietf/meeting/views.py
@@ -49,7 +49,7 @@ from django.views.generic import RedirectView
from ietf.doc.fields import SearchableDocumentsField
from ietf.doc.models import Document, State, DocEvent, NewRevisionDocEvent, DocAlias
from ietf.group.models import Group
-from ietf.group.utils import can_manage_session_materials
+from ietf.group.utils import can_manage_session_materials, can_manage_some_groups, can_manage_group
from ietf.person.models import Person
from ietf.person.name import plain_name
from ietf.ietfauth.utils import role_required, has_role
@@ -82,6 +82,7 @@ from ietf.secr.proceedings.utils import handle_upload_file
from ietf.secr.proceedings.proc_utils import (get_progress_stats, post_process, import_audio_files,
create_recording)
from ietf.utils.decorators import require_api_key
+from ietf.utils.history import find_history_replacements_active_at
from ietf.utils.log import assertion
from ietf.utils.mail import send_mail_message, send_mail_text
from ietf.utils.pipe import pipe
@@ -96,7 +97,7 @@ from .forms import (InterimMeetingModelForm, InterimAnnounceForm, InterimSession
def get_interim_menu_entries(request):
'''Setup menu entries for interim meeting view tabs'''
entries = []
- if has_role(request.user, ('Area Director','Secretariat','IRTF Chair','WG Chair', 'RG Chair')):
+ if can_manage_some_groups(request.user):
entries.append(("Upcoming", reverse("ietf.meeting.views.upcoming")))
entries.append(("Pending", reverse("ietf.meeting.views.interim_pending")))
if has_role(request.user, "Secretariat"):
@@ -601,7 +602,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
session_parents = sorted(set(
s.group.parent for s in sessions
- if s.group and s.group.parent and s.group.parent.type_id == 'area' or s.group.parent.acronym == 'irtf'
+ if s.group and s.group.parent and (s.group.parent.type_id == 'area' or s.group.parent.acronym == 'irtf')
), key=lambda p: p.acronym)
for i, p in enumerate(session_parents):
rgb_color = cubehelix(i, len(session_parents))
@@ -609,7 +610,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
# dig out historic AD names
ad_names = {}
- session_groups = set(s.group for s in sessions if s.group and s.group.parent.type_id == 'area')
+ session_groups = set(s.group for s in sessions if s.group and s.group.parent and s.group.parent.type_id == 'area')
meeting_time = datetime.datetime.combine(meeting.date, datetime.time(0, 0, 0))
for group_id, history_time, name in Person.objects.filter(rolehistory__name='ad', rolehistory__group__group__in=session_groups, rolehistory__group__time__lte=meeting_time).values_list('rolehistory__group__group', 'rolehistory__group__time', 'name').order_by('rolehistory__group__time'):
@@ -1508,7 +1509,7 @@ def meeting_requests(request, num=None):
s.current_status_name = status_names.get(s.current_status, s.current_status)
s.requested_by_person = session_requesters.get(s.requested_by)
- groups_not_meeting = Group.objects.filter(state='Active',type__in=['wg','rg','ag','bof']).exclude(acronym__in = [session.group.acronym for session in sessions]).order_by("parent__acronym","acronym").prefetch_related("parent")
+ groups_not_meeting = Group.objects.filter(state='Active',type__in=['wg','rg','ag','bof','program']).exclude(acronym__in = [session.group.acronym for session in sessions]).order_by("parent__acronym","acronym").prefetch_related("parent")
return render(request, "meeting/requests.html",
{"meeting": meeting, "sessions":sessions,
@@ -1532,9 +1533,22 @@ def session_details(request, num, acronym):
if not sessions:
raise Http404
+ # Find the time of the meeting, so that we can look back historically
+ # for what the group was called at the time.
+ meeting_time = datetime.datetime.combine(meeting.date, datetime.time())
+
+ groups = list(set([ s.group for s in sessions ]))
+ group_replacements = find_history_replacements_active_at(groups, meeting_time)
+
status_names = {n.slug: n.name for n in SessionStatusName.objects.all()}
for session in sessions:
+ session.historic_group = None
+ if session.group:
+ session.historic_group = group_replacements.get(session.group_id)
+ if session.historic_group:
+ session.historic_group.historic_parent = None
+
session.type_counter = Counter()
ss = session.timeslotassignments.filter(schedule=meeting.schedule).order_by('timeslot__time')
if ss:
@@ -1587,6 +1601,7 @@ def session_details(request, num, acronym):
'can_manage_materials' : can_manage,
'can_view_request': can_view_request,
'thisweek': datetime.date.today()-datetime.timedelta(days=7),
+ 'now': datetime.datetime.now(),
})
class SessionDraftsForm(forms.Form):
@@ -2392,8 +2407,12 @@ def interim_skip_announcement(request, number):
'meeting': meeting})
-@role_required('Area Director', 'Secretariat', 'IRTF Chair', 'WG Chair', 'RG Chair')
+@login_required
def interim_pending(request):
+
+ if not can_manage_some_groups(request.user):
+ return HttpResponseForbidden()
+
'''View which shows interim meeting requests pending approval'''
meetings = data_for_meetings_overview(Meeting.objects.filter(type='interim').order_by('date'), interim_status='apprw')
@@ -2411,8 +2430,12 @@ def interim_pending(request):
'meetings': meetings})
-@role_required('Area Director', 'Secretariat', 'IRTF Chair', 'WG Chair', 'RG Chair')
+@login_required
def interim_request(request):
+
+ if not can_manage_some_groups(request.user):
+ return HttpResponseForbidden("You don't have permission to request any interims")
+
'''View for requesting an interim meeting'''
SessionFormset = inlineformset_factory(
Meeting,
@@ -2497,15 +2520,15 @@ def interim_request(request):
"formset": formset})
-@role_required('Area Director', 'Secretariat', 'IRTF Chair', 'WG Chair', 'RG Chair')
+@login_required
def interim_request_cancel(request, number):
'''View for cancelling an interim meeting request'''
meeting = get_object_or_404(Meeting, number=number)
first_session = meeting.session_set.first()
- session_status = current_session_status(first_session)
group = first_session.group
- if not can_view_interim_request(meeting, request.user):
+ if not can_manage_group(request.user, group):
return HttpResponseForbidden("You do not have permissions to cancel this meeting request")
+ session_status = current_session_status(first_session)
if request.method == 'POST':
form = InterimCancelForm(request.POST)
@@ -2538,10 +2561,13 @@ def interim_request_cancel(request, number):
})
-@role_required('Area Director', 'Secretariat', 'IRTF Chair', 'WG Chair', 'RG Chair')
+@login_required
def interim_request_details(request, number):
- '''View details of an interim meeting reqeust'''
+ '''View details of an interim meeting request'''
meeting = get_object_or_404(Meeting, number=number)
+ group = meeting.session_set.first().group
+ if not can_manage_group(request.user, group):
+ return HttpResponseForbidden("You do not have permissions to manage this meeting request")
sessions = meeting.session_set.all()
can_edit = can_edit_interim_request(meeting, request.user)
can_approve = can_approve_interim_request(meeting, request.user)
@@ -2582,7 +2608,7 @@ def interim_request_details(request, number):
"can_approve": can_approve})
-@role_required('Area Director', 'Secretariat', 'IRTF Chair', 'WG Chair', 'RG Chair')
+@login_required
def interim_request_edit(request, number):
'''Edit details of an interim meeting reqeust'''
meeting = get_object_or_404(Meeting, number=number)
@@ -2645,7 +2671,7 @@ def past(request):
def upcoming(request):
'''List of upcoming meetings'''
- today = datetime.date.today()
+ today = datetime.date.today()-datetime.timedelta(days=7)
# Get ietf meetings starting 7 days ago, and interim meetings starting today
ietf_meetings = Meeting.objects.filter(type_id='ietf', date__gte=today-datetime.timedelta(days=7))
@@ -2684,6 +2710,7 @@ def upcoming(request):
'menu_actions': actions,
'menu_entries': menu_entries,
'selected_menu_entry': selected_menu_entry,
+ 'now': datetime.datetime.now()
})
diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json
index 99cb6a4bf..5fabc00aa 100644
--- a/ietf/name/fixtures/names.json
+++ b/ietf/name/fixtures/names.json
@@ -1,12 +1,12 @@
[
{
"fields": {
- "content": "{% autoescape off %}{{ assigner.ascii }} has assigned {{ reviewer.person.ascii }} as a reviewer for this document.\n\n{% if prev_team_reviews %}This team has completed other reviews of this document:{% endif %}{% for assignment in prev_team_reviews %}\n- {{ assignment.completed_on }} {{ assignment.reviewer.person.ascii }} -{% if assignment.reviewed_rev %}{{ assignment.reviewed_rev }}{% else %}{{ assignment.review_request.requested_rev }}{% endif %} {{ assignment.result.name }} \n{% endfor %}{% endautoescape %}\n",
+ "content": "{% autoescape off %}{{ assigner.ascii }} has assigned {{ reviewer.person.ascii }} as a reviewer for this document.\r\n\r\n{% if prev_team_reviews %}This team has completed other reviews of this document:{% endif %}{% for assignment in prev_team_reviews %}\r\n- {{ assignment.completed_on }} {{ assignment.reviewer.person.ascii }} -{% if assignment.reviewed_rev %}{{ assignment.reviewed_rev }}{% else %}{{ assignment.review_request.requested_rev }}{% endif %} {{ assignment.result.name }} \r\n{% endfor %}{% endautoescape %}",
"group": null,
"path": "/group/defaults/email/review_assigned.txt",
"title": "Default template for review assignment email",
"type": "django",
- "variables": null
+ "variables": ""
},
"model": "dbtemplate.dbtemplate",
"pk": 354
@@ -2484,6 +2484,7 @@
"customize_workflow": false,
"default_tab": "ietf.group.views.group_about",
"docman_roles": "[\"chair\"]",
+ "groupman_authroles": "[\"Secretariat\"]",
"groupman_roles": "[\"chair\",\"lead\",\"delegate\"]",
"has_chartering_process": false,
"has_default_jabber": true,
@@ -2514,6 +2515,7 @@
"customize_workflow": false,
"default_tab": "ietf.group.views.group_about",
"docman_roles": "[\"chair\"]",
+ "groupman_authroles": "[\"Secretariat\"]",
"groupman_roles": "[\"chair\"]",
"has_chartering_process": false,
"has_default_jabber": false,
@@ -2544,6 +2546,7 @@
"customize_workflow": false,
"default_tab": "ietf.group.views.group_about",
"docman_roles": "[\"chair\",\"delegate\",\"secr\"]",
+ "groupman_authroles": "[\"Secretariat\",\"Area Director\"]",
"groupman_roles": "[\"ad\",\"chair\",\"delegate\"]",
"has_chartering_process": false,
"has_default_jabber": false,
@@ -2574,6 +2577,7 @@
"customize_workflow": false,
"default_tab": "ietf.group.views.group_about",
"docman_roles": "[\"ad\",\"delegate\",\"secr\"]",
+ "groupman_authroles": "[\"Secretariat\"]",
"groupman_roles": "[\"ad\"]",
"has_chartering_process": false,
"has_default_jabber": false,
@@ -2604,6 +2608,7 @@
"customize_workflow": false,
"default_tab": "ietf.group.views.group_about",
"docman_roles": "[]",
+ "groupman_authroles": "[\"Secretariat\"]",
"groupman_roles": "[\"ad\",\"secr\"]",
"has_chartering_process": false,
"has_default_jabber": false,
@@ -2634,6 +2639,7 @@
"customize_workflow": false,
"default_tab": "ietf.group.views.group_about",
"docman_roles": "[\"chair\"]",
+ "groupman_authroles": "[\"Secretariat\"]",
"groupman_roles": "[]",
"has_chartering_process": false,
"has_default_jabber": false,
@@ -2664,6 +2670,7 @@
"customize_workflow": false,
"default_tab": "ietf.group.views.group_about",
"docman_roles": "[\"chair\"]",
+ "groupman_authroles": "[\"Secretariat\"]",
"groupman_roles": "[\"chair\"]",
"has_chartering_process": false,
"has_default_jabber": false,
@@ -2694,6 +2701,7 @@
"customize_workflow": false,
"default_tab": "ietf.group.views.group_about",
"docman_roles": "[\"chair\"]",
+ "groupman_authroles": "[\"Secretariat\"]",
"groupman_roles": "[\"chair\",\"delegate\"]",
"has_chartering_process": false,
"has_default_jabber": false,
@@ -2724,6 +2732,7 @@
"customize_workflow": false,
"default_tab": "ietf.group.views.group_about",
"docman_roles": "[\"chair\"]",
+ "groupman_authroles": "[\"Secretariat\"]",
"groupman_roles": "[\"chair\",\"delegate\"]",
"has_chartering_process": false,
"has_default_jabber": false,
@@ -2754,6 +2763,7 @@
"customize_workflow": false,
"default_tab": "ietf.group.views.group_about",
"docman_roles": "[\"auth\"]",
+ "groupman_authroles": "[]",
"groupman_roles": "[]",
"has_chartering_process": false,
"has_default_jabber": false,
@@ -2784,6 +2794,7 @@
"customize_workflow": false,
"default_tab": "ietf.group.views.group_about",
"docman_roles": "[]",
+ "groupman_authroles": "[\"Secretariat\"]",
"groupman_roles": "[\"chair\",\"delegate\"]",
"has_chartering_process": false,
"has_default_jabber": false,
@@ -2814,6 +2825,7 @@
"customize_workflow": false,
"default_tab": "ietf.group.views.group_about",
"docman_roles": "[\"chair\"]",
+ "groupman_authroles": "[\"Secretariat\"]",
"groupman_roles": "[\"chair\",\"delegate\"]",
"has_chartering_process": false,
"has_default_jabber": false,
@@ -2844,6 +2856,7 @@
"customize_workflow": false,
"default_tab": "ietf.group.views.group_about",
"docman_roles": "[]",
+ "groupman_authroles": "[\"Secretariat\"]",
"groupman_roles": "[\"chair\"]",
"has_chartering_process": false,
"has_default_jabber": false,
@@ -2874,6 +2887,7 @@
"customize_workflow": false,
"default_tab": "ietf.group.views.group_about",
"docman_roles": "[\"chair\"]",
+ "groupman_authroles": "[\"Secretariat\"]",
"groupman_roles": "[\"chair\",\"advisor\"]",
"has_chartering_process": false,
"has_default_jabber": false,
@@ -2903,21 +2917,22 @@
"custom_group_roles": true,
"customize_workflow": false,
"default_tab": "ietf.group.views.group_about",
- "docman_roles": "[\"lead\",\"secr\"]",
- "groupman_roles": "[\"lead\",\"secr\"]",
+ "docman_roles": "[\"lead\",\"chair\",\"secr\"]",
+ "groupman_authroles": "[\"Secretariat\",\"IAB\"]",
+ "groupman_roles": "[\"lead\",\"chair\",\"secr\"]",
"has_chartering_process": false,
"has_default_jabber": false,
"has_documents": true,
- "has_meetings": false,
+ "has_meetings": true,
"has_milestones": true,
"has_nonsession_materials": false,
"has_reviews": false,
"has_session_materials": false,
"is_schedulable": false,
"material_types": "[\"slides\"]",
- "matman_roles": "[\"lead\",\"secr\"]",
+ "matman_roles": "[\"lead\",\"chair\",\"secr\"]",
"req_subm_approval": false,
- "role_order": "[\"lead\",\"secr\"]",
+ "role_order": "[\"lead\",\"chair\",\"secr\"]",
"show_on_agenda": false
},
"model": "group.groupfeatures",
@@ -2934,6 +2949,7 @@
"customize_workflow": false,
"default_tab": "ietf.group.views.review_requests",
"docman_roles": "[\"secr\"]",
+ "groupman_authroles": "[\"Secretariat\"]",
"groupman_roles": "[\"ad\",\"secr\"]",
"has_chartering_process": false,
"has_default_jabber": false,
@@ -2964,6 +2980,7 @@
"customize_workflow": false,
"default_tab": "ietf.group.views.group_about",
"docman_roles": "[]",
+ "groupman_authroles": "[\"Secretariat\"]",
"groupman_roles": "[]",
"has_chartering_process": false,
"has_default_jabber": false,
@@ -2994,6 +3011,7 @@
"customize_workflow": true,
"default_tab": "ietf.group.views.group_documents",
"docman_roles": "[\"chair\",\"delegate\",\"secr\"]",
+ "groupman_authroles": "[\"Secretariat\",\"IRTF Chair\"]",
"groupman_roles": "[\"chair\",\"delegate\"]",
"has_chartering_process": true,
"has_default_jabber": true,
@@ -3024,6 +3042,7 @@
"customize_workflow": false,
"default_tab": "ietf.group.views.group_about",
"docman_roles": "[\"liaiman\",\"matman\"]",
+ "groupman_authroles": "[\"Secretariat\"]",
"groupman_roles": "[]",
"has_chartering_process": false,
"has_default_jabber": false,
@@ -3054,6 +3073,7 @@
"customize_workflow": false,
"default_tab": "ietf.group.views.group_about",
"docman_roles": "[\"chair\"]",
+ "groupman_authroles": "[\"Secretariat\"]",
"groupman_roles": "[\"chair\"]",
"has_chartering_process": false,
"has_default_jabber": false,
@@ -3084,6 +3104,7 @@
"customize_workflow": true,
"default_tab": "ietf.group.views.group_documents",
"docman_roles": "[\"chair\",\"delegate\",\"secr\"]",
+ "groupman_authroles": "[\"Secretariat\",\"Area Director\"]",
"groupman_roles": "[\"ad\",\"chair\",\"delegate\",\"secr\"]",
"has_chartering_process": true,
"has_default_jabber": true,
@@ -5603,8 +5624,8 @@
},
{
"fields": {
- "editor_label": "(1)",
"desc": "",
+ "editor_label": "(1)",
"name": "Conflicts with",
"order": 0,
"penalty": 100000,
@@ -5616,9 +5637,10 @@
{
"fields": {
"desc": "",
+ "editor_label": "time_relation",
"name": "Preference for time between sessions",
"order": 0,
- "penalty": 100000,
+ "penalty": 1000,
"used": true
},
"model": "name.constraintname",
@@ -5627,6 +5649,7 @@
{
"fields": {
"desc": "",
+ "editor_label": "timerange",
"name": "Can't meet within timerange",
"order": 0,
"penalty": 100000,
@@ -5638,9 +5661,10 @@
{
"fields": {
"desc": "",
+ "editor_label": "wg_adjacent",
"name": "Request for adjacent scheduling with another WG",
"order": 0,
- "penalty": 100000,
+ "penalty": 10000,
"used": true
},
"model": "name.constraintname",
@@ -9065,7 +9089,7 @@
"desc": "IESG discussions on the document have raised some issues that need to be brought to the attention of the authors/WG, but those issues have not been written down yet. (It is common for discussions during a telechat to result in such situations. An AD may raise a possible issue during a telechat and only decide as a result of that discussion whether the issue is worth formally writing up and bringing to the attention of the authors/WG). A document stays in the \"Point Raised - Writeup Needed\" state until *ALL* IESG comments that have been raised have been documented.",
"name": "Point Raised - writeup needed",
"order": 1,
- "used": true
+ "used": false
},
"model": "name.doctagname",
"pk": "point"
@@ -10181,6 +10205,17 @@
"model": "name.importantdatename",
"pk": "idcutoff"
},
+ {
+ "fields": {
+ "default_offset_days": 70,
+ "desc": "Announcement of whether conditions have improved enough to hold an in-person meeting in Madrid, or if IETF 108 will be held as a virtual meeting",
+ "name": "IETF 108 Go-ahead Announcement",
+ "order": 0,
+ "used": false
+ },
+ "model": "name.importantdatename",
+ "pk": "ietf-108-go-ahead"
+ },
{
"fields": {
"default_offset_days": -82,
@@ -11268,6 +11303,16 @@
"model": "name.rolename",
"pk": "chair"
},
+ {
+ "fields": {
+ "desc": "",
+ "name": "Communications Director",
+ "order": 0,
+ "used": true
+ },
+ "model": "name.rolename",
+ "pk": "comdir"
+ },
{
"fields": {
"desc": "",
@@ -11420,7 +11465,7 @@
},
{
"fields": {
- "desc": "Provides log-in permission to restricted Trac instances",
+ "desc": "Provides log-in permission to restricted Trac instances. Used by the generate_apache_perms management command, called from ../../scripts/Cron-runner",
"name": "Trac Editor",
"order": 0,
"used": true
@@ -11500,8 +11545,8 @@
},
{
"fields": {
- "desc": "WebEx support",
- "name": "WebEx session",
+ "desc": "Web streaming support",
+ "name": "WebEx",
"order": 0,
"used": true
},
@@ -14436,9 +14481,9 @@
"fields": {
"command": "xym",
"switch": "--version",
- "time": "2020-02-19T00:13:43.554",
+ "time": "2020-05-10T00:12:51.809",
"used": true,
- "version": "xym 0.4"
+ "version": "xym 0.4.8"
},
"model": "utils.versioninfo",
"pk": 1
@@ -14447,9 +14492,9 @@
"fields": {
"command": "pyang",
"switch": "--version",
- "time": "2020-02-19T00:13:44.450",
+ "time": "2020-05-10T00:12:53.489",
"used": true,
- "version": "pyang 2.1.1"
+ "version": "pyang 2.2.1"
},
"model": "utils.versioninfo",
"pk": 2
@@ -14458,9 +14503,9 @@
"fields": {
"command": "yanglint",
"switch": "--version",
- "time": "2020-02-19T00:13:44.597",
+ "time": "2020-05-10T00:12:53.919",
"used": true,
- "version": "yanglint 0.14.80"
+ "version": "yanglint SO 1.6.7"
},
"model": "utils.versioninfo",
"pk": 3
@@ -14469,9 +14514,9 @@
"fields": {
"command": "xml2rfc",
"switch": "--version",
- "time": "2020-02-19T00:13:45.481",
+ "time": "2020-05-10T00:12:56.462",
"used": true,
- "version": "xml2rfc 2.40.0"
+ "version": "xml2rfc 2.44.0"
},
"model": "utils.versioninfo",
"pk": 4
diff --git a/ietf/person/models.py b/ietf/person/models.py
index a79487b43..d48206972 100644
--- a/ietf/person/models.py
+++ b/ietf/person/models.py
@@ -353,7 +353,10 @@ class PersonalApiKey(models.Model):
def validate_key(cls, s):
import struct, hashlib, base64
assert isinstance(s, bytes)
- key = base64.urlsafe_b64decode(s)
+ try:
+ key = base64.urlsafe_b64decode(s)
+ except Exception:
+ return None
id, salt, hash = struct.unpack(KEY_STRUCT, key)
k = cls.objects.filter(id=id)
if not k.exists():
diff --git a/ietf/secr/sreq/views.py b/ietf/secr/sreq/views.py
index 0ff31bdb4..7fd934fe7 100644
--- a/ietf/secr/sreq/views.py
+++ b/ietf/secr/sreq/views.py
@@ -29,7 +29,8 @@ from ietf.mailtrigger.utils import gather_address_lists
# -------------------------------------------------
# Globals
# -------------------------------------------------
-AUTHORIZED_ROLES=('WG Chair','WG Secretary','RG Chair','IAB Group Chair','Area Director','Secretariat','Team Chair','IRTF Chair')
+# TODO: This needs to be replaced with something that pays attention to groupfeatures
+AUTHORIZED_ROLES=('WG Chair','WG Secretary','RG Chair','IAB Group Chair','Area Director','Secretariat','Team Chair','IRTF Chair','Program Chair','Program Lead','Program Secretary')
# -------------------------------------------------
# Helper Functions
@@ -319,7 +320,10 @@ def confirm(request, acronym):
)
if 'resources' in form.data:
new_session.resources.set(session_data['resources'])
- if int(form.data.get('joint_for_session', '-1')) == count:
+ jfs = form.data.get('joint_for_session', '-1')
+ if not jfs: # jfs might be ''
+ jfs = '-1'
+ if int(jfs) == count:
groups_split = form.cleaned_data.get('joint_with_groups').replace(',',' ').split()
joint = Group.objects.filter(acronym__in=groups_split)
new_session.joint_with_groups.set(joint)
diff --git a/ietf/static/ietf/css/ietf.css b/ietf/static/ietf/css/ietf.css
index 251cadb83..e11b8a34c 100644
--- a/ietf/static/ietf/css/ietf.css
+++ b/ietf/static/ietf/css/ietf.css
@@ -771,6 +771,25 @@ ul.progress-section {
.btn .fa-stack { width: 1em; height: 1em; }
.btn .fa-stack .fa-stack-1x { line-height: 80%; }
+.fa-stack-1 {
+ position: relative;
+ display: inline-block;
+ width: 1.28571429em;
+ height: 1em;
+ vertical-align: inherit;
+}
+.fa-stack-sm {
+ width: 100%;
+ text-align: center;
+ font-size: 0.7172em;
+ line-height: inherit;
+}
+.fa-stack-xs {
+ width: 100%;
+ text-align: center;
+ font-size: 0.5em;
+ line-height: inherit;
+}
/* ========================================================================== */
diff --git a/ietf/static/ietf/font-datatracker/README.txt b/ietf/static/ietf/font-datatracker/README.txt
new file mode 100755
index 000000000..beaab3366
--- /dev/null
+++ b/ietf/static/ietf/font-datatracker/README.txt
@@ -0,0 +1,75 @@
+This webfont is generated by http://fontello.com open source project.
+
+
+================================================================================
+Please, note, that you should obey original font licenses, used to make this
+webfont pack. Details available in LICENSE.txt file.
+
+- Usually, it's enough to publish content of LICENSE.txt file somewhere on your
+ site in "About" section.
+
+- If your project is open-source, usually, it will be ok to make LICENSE.txt
+ file publicly available in your repository.
+
+- Fonts, used in Fontello, don't require a clickable link on your site.
+ But any kind of additional authors crediting is welcome.
+================================================================================
+
+
+Comments on archive content
+---------------------------
+
+- /font/* - fonts in different formats
+
+- /css/* - different kinds of css, for all situations. Should be ok with
+ twitter bootstrap. Also, you can skip style and assign icon classes
+ directly to text elements, if you don't mind about IE7.
+
+- demo.html - demo file, to show your webfont content
+
+- LICENSE.txt - license info about source fonts, used to build your one.
+
+- config.json - keeps your settings. You can import it back into fontello
+ anytime, to continue your work
+
+
+Why so many CSS files ?
+-----------------------
+
+Because we like to fit all your needs :)
+
+- basic file, doc.json
, e.g.,
{% url 'ietf.doc.views_doc.document_json' name='draft-ietf-poised95-std-proc-3' %}
- .
+ .
You can also specify an RFC: {% url 'ietf.doc.views_doc.document_json' name='rfc2026' %}
- .
+ .
No API key is needed to access this.
from jwcrypto import jwk, jws @@ -339,7 +340,6 @@ jwstoken.deserialize(data) jwstoken.verify(key) payload = jwstoken.payload- diff --git a/ietf/templates/base.html b/ietf/templates/base.html index 9cbf25784..bdefe9d63 100644 --- a/ietf/templates/base.html +++ b/ietf/templates/base.html @@ -19,6 +19,7 @@ {% endcomment %} + diff --git a/ietf/templates/meeting/agenda.html b/ietf/templates/meeting/agenda.html index 9e85116d2..8897ae8e0 100644 --- a/ietf/templates/meeting/agenda.html +++ b/ietf/templates/meeting/agenda.html @@ -229,15 +229,12 @@ CANCELLED {% endif %} - {% if item.session.agenda %} - {% include "meeting/session_agenda_include.html" %} - {% endif %} -