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, .css - is usually enough, it contains @font-face + and character code definitions + +- *-ie7.css - if you need IE7 support, but still don't wish to put char codes + directly into html + +- *-codes.css and *-ie7-codes.css - if you like to use your own @font-face + rules, but still wish to benefit from css generation. That can be very + convenient for automated asset build systems. When you need to update font - + no need to manually edit files, just override old version with archive + content. See fontello source code for examples. + +- *-embedded.css - basic css file, but with embedded WOFF font, to avoid + CORS issues in Firefox and IE9+, when fonts are hosted on the separate domain. + We strongly recommend to resolve this issue by `Access-Control-Allow-Origin` + server headers. But if you ok with dirty hack - this file is for you. Note, + that data url moved to separate @font-face to avoid problems with + + +Copyright (C) 2020 by original authors @ fontello.com + + + + + + + + \ No newline at end of file diff --git a/ietf/static/ietf/font-datatracker/font/datatracker.ttf b/ietf/static/ietf/font-datatracker/font/datatracker.ttf new file mode 100755 index 000000000..d7ab48679 Binary files /dev/null and b/ietf/static/ietf/font-datatracker/font/datatracker.ttf differ diff --git a/ietf/static/ietf/font-datatracker/font/datatracker.woff b/ietf/static/ietf/font-datatracker/font/datatracker.woff new file mode 100755 index 000000000..dc0d089a1 Binary files /dev/null and b/ietf/static/ietf/font-datatracker/font/datatracker.woff differ diff --git a/ietf/static/ietf/font-datatracker/font/datatracker.woff2 b/ietf/static/ietf/font-datatracker/font/datatracker.woff2 new file mode 100755 index 000000000..239ca87fd Binary files /dev/null and b/ietf/static/ietf/font-datatracker/font/datatracker.woff2 differ diff --git a/ietf/static/ietf/font-datatracker/font/font-datatracker.eot b/ietf/static/ietf/font-datatracker/font/font-datatracker.eot new file mode 100755 index 000000000..b0f35e502 Binary files /dev/null and b/ietf/static/ietf/font-datatracker/font/font-datatracker.eot differ diff --git a/ietf/static/ietf/font-datatracker/font/font-datatracker.svg b/ietf/static/ietf/font-datatracker/font/font-datatracker.svg new file mode 100755 index 000000000..cbd2186a6 --- /dev/null +++ b/ietf/static/ietf/font-datatracker/font/font-datatracker.svg @@ -0,0 +1,12 @@ + + + +Copyright (C) 2020 by original authors @ fontello.com + + + + + + + + \ No newline at end of file diff --git a/ietf/static/ietf/font-datatracker/font/font-datatracker.ttf b/ietf/static/ietf/font-datatracker/font/font-datatracker.ttf new file mode 100755 index 000000000..d7ab48679 Binary files /dev/null and b/ietf/static/ietf/font-datatracker/font/font-datatracker.ttf differ diff --git a/ietf/static/ietf/font-datatracker/font/font-datatracker.woff b/ietf/static/ietf/font-datatracker/font/font-datatracker.woff new file mode 100755 index 000000000..dc0d089a1 Binary files /dev/null and b/ietf/static/ietf/font-datatracker/font/font-datatracker.woff differ diff --git a/ietf/static/ietf/font-datatracker/font/font-datatracker.woff2 b/ietf/static/ietf/font-datatracker/font/font-datatracker.woff2 new file mode 100755 index 000000000..239ca87fd Binary files /dev/null and b/ietf/static/ietf/font-datatracker/font/font-datatracker.woff2 differ diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py index 057054842..41c872bd0 100644 --- a/ietf/submit/utils.py +++ b/ietf/submit/utils.py @@ -133,7 +133,8 @@ def validate_submission_rev(name, rev): expected = 0 existing_revs = [int(i.rev) for i in Document.objects.filter(name=name) if i.rev and i.rev.isdigit() ] - log.assertion('[ i.rev for i in Document.objects.filter(name=name) if not (i.rev and i.rev.isdigit()) ]', []) + unexpected_revs = [ i.rev for i in Document.objects.filter(name=name) if not (i.rev and i.rev.isdigit()) ] # pyflakes:ignore + log.assertion('unexpected_revs', []) if existing_revs: expected = max(existing_revs) + 1 diff --git a/ietf/templates/api/index.html b/ietf/templates/api/index.html index 3afee7b69..1bf06a692 100644 --- a/ietf/templates/api/index.html +++ b/ietf/templates/api/index.html @@ -178,9 +178,9 @@ document attributes are necessary for an application is available for all documents at the relative url 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.

@@ -327,6 +327,7 @@ To verify a signature and get the verified data using Python with the jwcrypto module, you could do: +

 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 %} - + {% endif %} + {% else %} + + + + + {% with session.recordings as recordings %} + {% if recordings %} + {# There's no guaranteed order, so this is a bit messy: #} + + {% for r in recordings %}{% with href=r.get_href %} + {% if 'audio' in href %} + + {% endif %} + {% endwith %}{% endfor %} + + {% for r in recordings %}{% with href=r.get_href %} + {% if 'youtu' in href %} + + {% endif %} + {% endwith %}{% endfor %} + + {% for r in recordings %}{% with href=r.get_href %} + {% if not 'audio' in href and not 'youtu' in href %} + + {% endif %} + {% endwith %}{% endfor %} + {% elif item.timeslot.location.video_stream_url %} + + {% elif show_empty %} + + {% endif %} + {% endwith %} + {% endif %} + + \ No newline at end of file diff --git a/ietf/templates/meeting/session_buttons_include.html b/ietf/templates/meeting/session_buttons_include.html index 6570556dd..8e5389345 100644 --- a/ietf/templates/meeting/session_buttons_include.html +++ b/ietf/templates/meeting/session_buttons_include.html @@ -2,112 +2,95 @@ {% load origin %} {% load staticfiles %} {% load textfilters %} - {% origin %} -{% if item.session.agenda %} - - - -{% endif %} +
+ {% if session.agenda and show_agenda %} + {% include "meeting/session_agenda_include.html" %} + + + + + + + {% endif %} -{% if item.timeslot.type.slug == 'plenary' %} - -{% else %} - -{% endif %} + + {% if item.timeslot.type.slug == 'plenary' %} + + {% else %} + + {% endif %} -{# show stream buttons up till end of session, then show archive buttons #} -{% if now < item.timeslot.end_time %} - - - - - - {% if item.timeslot.location.video_stream_url %} - - - {% endif %} - - {% if item.timeslot.location.audio_stream_url %} - - - {% endif %} - - {% if "https://ietf.webex.com" in item.session.agenda_note|first_url %} - - - {% else %} - {% if item.timeslot.location.webex_url %} - - - {% endif %} - {% endif %} - -{% else %} - - - {% if schedule.meeting.number|add:"0" >= 60 %} - - - - - - - {% endif %} - - - {% if schedule.meeting.number|add:"0" >= 80 %} - {% with item.session.recordings as recordings %} - {% if recordings %} - - - {% for r in recordings %} - {% if r.get_href and 'audio' in r.get_href %} - - - - {% endif %} - {% endfor %} - - {% for r in recordings %} - {% if r.get_href and 'youtu' in r.get_href %} - - - - {% endif %} - {% endfor %} - - {% for r in recordings %} - {% if r.get_href and not 'audio' in r.get_href and not 'youtu' in r.get_href %} - - - - {% endif %} - {% endfor %} - {% endif %} - {% if item.timeslot.location.video_stream_url %} - Meetecho Stream - + {# show stream buttons up till end of session, then show archive buttons #} + {% if now < item.timeslot.end_time %} + + + + {% if item.timeslot.location.video_stream_url %} + + + {% endif %} + + {% if item.timeslot.location.audio_stream_url %} + + + {% endif %} + + {% if "webex.com/" in session.agenda_note|first_url %} + + + {% elif item.timeslot.location.webex_url %} + + + {% endif %} + {% else %} + + {% if meeting.number|add:"0" >= 60 %} + + + + {% endif %} + + {% if meeting.number|add:"0" >= 80 %} + {% with session.recordings as recordings %} + {% if recordings %} + {# There's no guaranteed order, so this is a bit messy: #} + + {% for r in recordings %} + {% if r.get_href and 'audio' in r.get_href %} + {% endif %} - - {% endwith %} - {% endif %} - - - {# #} -{% endif %} + {% endfor %} + + {% for r in recordings %} + {% if r.get_href and 'youtu' in r.get_href %} + + {% endif %} + {% endfor %} + + {% for r in recordings %} + {% if r.get_href and not 'audio' in r.get_href and not 'youtu' in r.get_href %} + + {% endif %} + {% endfor %} + {% endif %} + {% endwith %} + {% if item.timeslot.location.video_stream_url %} + + {% endif %} + {% endif %} + {% endif %} +
\ No newline at end of file diff --git a/ietf/templates/meeting/session_details.html b/ietf/templates/meeting/session_details.html index 0415f0b84..d8b821929 100644 --- a/ietf/templates/meeting/session_details.html +++ b/ietf/templates/meeting/session_details.html @@ -17,7 +17,7 @@

{{ meeting }} : {{ acronym }} {% if meeting.date >= thisweek %} - + {% endif %}

diff --git a/ietf/templates/meeting/session_details_panel.html b/ietf/templates/meeting/session_details_panel.html index 99dc7cb62..3a87dff7f 100644 --- a/ietf/templates/meeting/session_details_panel.html +++ b/ietf/templates/meeting/session_details_panel.html @@ -1,144 +1,158 @@ {% load origin ietf_filters textfilters tz dateformat %}{% origin %} {% for session in sessions %} -

{% if sessions|length > 1 %}Session {{ forloop.counter }} : {% endif %} - {% for time in session.times %}{% if not forloop.first %}, {% endif %} {{time|dateformat:"l Y-m-d H:i T"}} {% if time.tzinfo.zone != "UTC" %}({{time|utc|dateformat:"H:i T"}}){% endif %}{% endfor %} - {% if session.cancelled %}CANCELLED{% else %}{{ session.status }}{% endif %} - {% if session.name %} : {{ session.name }}{% endif %} - {% if meeting.date >= thisweek %} - - {% endif %} -

- {% if session.agenda_note %}

{{session.agenda_note}}

{% endif %} + {% with item=session.official_timeslotassignment %} +

{% if sessions|length > 1 %}Session {{ forloop.counter }} : {% endif %} + {% for time in session.times %}{% if not forloop.first %}, {% endif %} {{time|dateformat:"l Y-m-d H:i T"}} {% if time.tzinfo.zone != "UTC" %}({{time|utc|dateformat:"H:i T"}}){% endif %}{% endfor %} + {% if session.cancelled %}CANCELLED{% else %}{{ session.status }}{% endif %} + {% if session.name %} : {{ session.name }}{% endif %} + {% if not session.cancelled %} + + {% if meeting.type.slug == 'interim' %} + {% include "meeting/interim_session_buttons.html" with show_agenda=False show_empty=False %} + {% else %} + {% with schedule=meeting.schedule %} + {% include "meeting/session_buttons_include.html" %} + {% endwith %} + {% endif %} + + {% if now < item.timeslot.end_time %} + + {% endif %} + + {% endif %} +

+ {% if session.agenda_note %}

{{session.agenda_note}}

{% endif %} - {% if can_manage_materials %} - {% if session.current_status == 'sched' or session.current_status == 'schedw' %} -
- {% if can_view_request %} - Meeting Details + {% if can_manage_materials %} + {% if session.current_status == 'sched' or session.current_status == 'schedw' %} +
+ {% if can_view_request %} + Meeting Details + {% endif %} +
+ {% if not session.type_counter.agenda %} + This session does not yet have an agenda + {% endif %} + {% endif %} + {% endif %} + + {% if meeting.type.slug == 'interim' and session.remote_instructions %} +
+ Remote instructions: + {% if "https://ietf.webex.com" in session.agenda_note|first_url %} + + + {% elif "https://ietf.webex.com" in session.remote_instructions|first_url %} + + + {% endif %} + {{ session.remote_instructions }} +
+ {% endif %} + +
+
Agenda, Minutes, and Bluesheets
+
+ + {% for pres in session.filtered_artifacts %} + + {% url 'ietf.doc.views_doc.document_main' name=pres.document.name as url %} + + {% if user|has_role:"Secretariat" or can_manage_materials %} + + {% endif %} + + {% endfor %} +
+ {{pres.document.title}} + ({{ pres.document.name }}) + + {% if pres.document.type.slug == 'minutes' %} + {% url 'ietf.meeting.views.upload_session_minutes' session_id=session.pk num=session.meeting.number as upload_url %} + {% elif pres.document.type.slug == 'agenda' %} + {% url 'ietf.meeting.views.upload_session_agenda' session_id=session.pk num=session.meeting.number as upload_url %} + {% else %} + {% url 'ietf.meeting.views.upload_session_bluesheets' session_id=session.pk num=session.meeting.number as upload_url %} + {% endif %} + {% if pres.document.type.slug != 'bluesheets' or user|has_role:"Secretariat" or meeting.type.slug == 'interim' and can_manage_materials %} + Upload Revision + {% endif %} +
+ {% if can_manage_materials %} + {% if not session.type_counter.agenda %} + Upload Agenda + {% endif %} + {% if not session.type_counter.minutes %} + Upload Minutes + {% endif %} + {% endif %} + {% if user|has_role:"Secretariat" and not session.type_counter.bluesheets or meeting.type.slug == 'interim' and can_manage_materials and not session.type_counter.bluesheets %} + Upload Bluesheets {% endif %}
- {% if not session.type_counter.agenda %} - This session does not yet have an agenda - {% endif %} - {% endif %} - {% endif %} - - {% if meeting.type.slug == 'interim' and session.remote_instructions %} -
- Remote instructions: - {% if "https://ietf.webex.com" in session.agenda_note|first_url %} - - - {% elif "https://ietf.webex.com" in session.remote_instructions|first_url %} - - - {% endif %} - {{ session.remote_instructions }} -
- {% endif %} - -
-
Agenda, Minutes, and Bluesheets
-
- - {% for pres in session.filtered_artifacts %} - - {% url 'ietf.doc.views_doc.document_main' name=pres.document.name as url %} - - {% if user|has_role:"Secretariat" or can_manage_materials %} - - {% endif %} - - {% endfor %} -
- {{pres.document.title}} - ({{ pres.document.name }}) - - {% if pres.document.type.slug == 'minutes' %} - {% url 'ietf.meeting.views.upload_session_minutes' session_id=session.pk num=session.meeting.number as upload_url %} - {% elif pres.document.type.slug == 'agenda' %} - {% url 'ietf.meeting.views.upload_session_agenda' session_id=session.pk num=session.meeting.number as upload_url %} - {% else %} - {% url 'ietf.meeting.views.upload_session_bluesheets' session_id=session.pk num=session.meeting.number as upload_url %} - {% endif %} - {% if pres.document.type.slug != 'bluesheets' or user|has_role:"Secretariat" or meeting.type.slug == 'interim' and can_manage_materials %} - Upload Revision - {% endif %} -
- {% if can_manage_materials %} - {% if not session.type_counter.agenda %} - Upload Agenda - {% endif %} - {% if not session.type_counter.minutes %} - Upload Minutes - {% endif %} - {% endif %} - {% if user|has_role:"Secretariat" and not session.type_counter.bluesheets or meeting.type.slug == 'interim' and can_manage_materials and not session.type_counter.bluesheets %} - Upload Bluesheets - {% endif %}
-
-
-
Slides
-
- - - {% for pres in session.filtered_slides %} - - {% url 'ietf.doc.views_doc.document_main' name=pres.document.name as url %} - - {% if can_manage_materials %} - - {% endif %} - - {% endfor %} - -
- {{pres.document.title}} - ({{ pres.document.name }}) - - Upload Revision - Remove -
- {% if can_manage_materials %} - Upload New Slides - {% elif request.user.is_authenticated and not session.is_material_submission_cutoff %} - Propose Slides - {% endif %} -
- {% if can_manage_materials %} - - {% endif %} -
-
-
Drafts -
-
- - {% for pres in session.filtered_drafts %} - - + {% endif %} + + {% endfor %} + +
+
+
Slides
+
+ + + {% for pres in session.filtered_slides %} + {% url 'ietf.doc.views_doc.document_main' name=pres.document.name as url %} - {{pres.document.title}} ({{ pres.document.name }}) {% if pres.rev %}Version {{pres.rev}}{% endif %} - - {% if can_manage_materials %} - - {% endif %} - - {% endfor %} -
- Remove + + {{pres.document.title}} + ({{ pres.document.name }})
+ {% if can_manage_materials %} +
+ Upload Revision + Remove +
+ {% if can_manage_materials %} + Upload New Slides + {% elif request.user.is_authenticated and not session.is_material_submission_cutoff %} + Propose Slides + {% endif %} +
{% if can_manage_materials %} - - Link additional drafts to session - + {% endif %}
-
+
+
Drafts +
+
+ + {% for pres in session.filtered_drafts %} + + + {% if can_manage_materials %} + + {% endif %} + + {% endfor %} +
+ {% url 'ietf.doc.views_doc.document_main' name=pres.document.name as url %} + {{pres.document.title}} ({{ pres.document.name }}) {% if pres.rev %}Version {{pres.rev}}{% endif %} + + Remove +
+ {% if can_manage_materials %} + + Link additional drafts to session + + {% endif %} +
+
+ {% endwith %} {% endfor %} diff --git a/ietf/templates/meeting/upcoming.html b/ietf/templates/meeting/upcoming.html index 192c9ff09..bb32da7de 100644 --- a/ietf/templates/meeting/upcoming.html +++ b/ietf/templates/meeting/upcoming.html @@ -20,7 +20,14 @@
-

Upcoming Meetings

+

Upcoming Meetings + + + + + + +

For more on regular IETF meetings see here

@@ -60,7 +67,7 @@ {% with meeting=entry %} {{ meeting.date }} - {{ meeting.end }} ietf - IETF {{ meeting.number }} + IETF {{ meeting.number }} {% endwith %} {% elif entry|classname == 'Session' %} @@ -68,11 +75,17 @@ {{ session.official_timeslotassignment.timeslot.utc_start_time | date:"Y-m-d H:i"}} - {{ session.official_timeslotassignment.timeslot.utc_end_time | date:"H:i e" }} {{ session.group.acronym }} - {{ session.meeting.number }}{% if session.current_status == 'canceled' %}  CANCELLED{% endif %} - - - {% include "meeting/interim_session_buttons.html" %} + {{ session.meeting.number }} + {% if session.current_status == 'canceled' %} + + CANCELLED + + {% else %} + + {% include "meeting/interim_session_buttons.html" with show_agenda=True %} + + {% endif %} {% endwith %} {% else %} Unexpected entry type: {{entry|classname}} diff --git a/ietf/templates/nomcom/announcements.html b/ietf/templates/nomcom/announcements.html index 7626b369d..771ef8b00 100644 --- a/ietf/templates/nomcom/announcements.html +++ b/ietf/templates/nomcom/announcements.html @@ -63,7 +63,7 @@

References

diff --git a/ietf/templates/stats/document_stats_author_affiliation.html b/ietf/templates/stats/document_stats_author_affiliation.html index 9c2b07c49..e21397111 100644 --- a/ietf/templates/stats/document_stats_author_affiliation.html +++ b/ietf/templates/stats/document_stats_author_affiliation.html @@ -84,8 +84,10 @@ {% if alias_data %} - - + + + + {% for name, alias in alias_data %} diff --git a/ietf/templates/submit/problem-reports-footer.html b/ietf/templates/submit/problem-reports-footer.html index 066e24c93..faa48af02 100644 --- a/ietf/templates/submit/problem-reports-footer.html +++ b/ietf/templates/submit/problem-reports-footer.html @@ -1,9 +1,13 @@ {# Copyright The IETF Trust 2015, All Rights Reserved #}{% load origin %}{% origin %}

- Please send reports about submission problems to - ietf-action@ietf.org, or, if you feel - this is a bug, please report it to the Tools Team using the Bug Report links at the bottom - of the page. + Please send reports about submission tool bugs to the Tools Team using one + of the Bug Report links at the bottom of the page. + +

+

+ + If you need to request manual posting of an Internet-Draft, please send the + draft and the reason for manual posting to idsubmission@ietf.org.

diff --git a/ietf/templates/submit/tool_instructions.html b/ietf/templates/submit/tool_instructions.html index f463f4c24..9b9d8b5f5 100644 --- a/ietf/templates/submit/tool_instructions.html +++ b/ietf/templates/submit/tool_instructions.html @@ -162,14 +162,7 @@

Problem report

-

- - Please send reports about submission problems to - ietf-action@ietf.org, or, if you feel - there is a bug, please report it to the Tools Team using the Bug Report links at the bottom - of the page. - -

+ {% include "submit/problem-reports-footer.html" %}

The specification for this tool can be found in RFC 4228.

AffiliationAlias
AffiliationAlias