diff --git a/ietf/community/utils.py b/ietf/community/utils.py index b39e09a12..e2d284b59 100644 --- a/ietf/community/utils.py +++ b/ietf/community/utils.py @@ -47,12 +47,8 @@ def can_manage_community_list(user, clist): if has_role(user, 'Secretariat'): return True - if clist.group.type_id == 'area': - return Role.objects.filter(name__slug='ad', person__user=user, group=clist.group).exists() - elif clist.group.type_id in ('wg', 'rg', 'ag'): - return Role.objects.filter(name__slug='chair', person__user=user, group=clist.group).exists() - elif clist.group.type_id in ('program'): - return Role.objects.filter(name__slug='lead', person__user=user, group=clist.group).exists() + if clist.group.type_id in ['area', 'wg', 'rg', 'ag', 'program', ]: + return Role.objects.filter(name__slug__in=clist.group.features.admin_roles, person__user=user, group=clist.group).exists() return False diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index 6df5b76c5..9e5288c30 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -269,7 +269,9 @@ def generate_approval_mail_rfc_editor(request, doc): def generate_publication_request(request, doc): group_description = "" if doc.group and doc.group.acronym != "none": - group_description = doc.group.name_with_acronym() + group_description = doc.group.name + if doc.group.type_id not in ("ietf", "irtf", "iab",): + group_description += " %s (%s)" % (doc.group.type, doc.group.acronym) e = doc.latest_event(ConsensusDocEvent, type="changed_consensus") consensus = e.consensus if e else None diff --git a/ietf/doc/templatetags/managed_groups.py b/ietf/doc/templatetags/managed_groups.py index 8847a1fa4..19a38528d 100644 --- a/ietf/doc/templatetags/managed_groups.py +++ b/ietf/doc/templatetags/managed_groups.py @@ -1,6 +1,6 @@ from django import template -from ietf.group.models import Group +from ietf.group.models import Group, Role register = template.Library() @@ -9,18 +9,11 @@ def managed_groups(user): if not (user and hasattr(user, "is_authenticated") and user.is_authenticated): return [] - groups = [] - # groups.extend(Group.objects.filter( - # role__name__slug='ad', - # role__person__user=user, - # type__slug='area', - # state__slug='active').select_related("type")) - - groups.extend(Group.objects.filter( - role__name__slug__in=['chair', 'delegate', 'ad', ], - role__person__user=user, - type__slug__in=('rg', 'wg', 'ag', 'ietf'), - state__slug__in=('active', 'bof')).select_related("type")) + groups = [ g for g in Group.objects.filter( + role__person__user=user, + type__features__has_session_materials=True, + state__slug__in=('active', 'bof')).select_related("type") + if Role.objects.filter(group=g, person__user=user, name__slug__in=g.type.features.matman_roles) ] return groups diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index 1f7ab63a8..7fac55de8 100644 --- a/ietf/doc/tests.py +++ b/ietf/doc/tests.py @@ -887,17 +887,17 @@ class DocTestCase(TestCase): self.client.login(username='iab-chair', password='iab-chair+password') r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name))) self.assertEqual(r.status_code, 200) - self.assertTrue("Request publication" not in unicontent(r)) + self.assertNotIn("Request publication", unicontent(r)) Document.objects.filter(pk=doc.pk).update(stream='iab') r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name))) self.assertEqual(r.status_code, 200) - self.assertTrue("Request publication" in unicontent(r)) + self.assertIn("Request publication", unicontent(r)) doc.states.add(State.objects.get(type_id='draft-stream-iab',slug='rfc-edit')) r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name))) self.assertEqual(r.status_code, 200) - self.assertTrue("Request publication" not in unicontent(r)) + self.assertNotIn("Request publication", unicontent(r)) def test_document_bibtex(self): diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index 84e4a6b68..8ed85e528 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -1,4 +1,5 @@ -# Copyright The IETF Trust 2016, All Rights Reserved +# Copyright The IETF Trust 2016-2018, All Rights Reserved + # Parts Copyright (C) 2009-2010 Nokia Corporation and/or its subsidiary(-ies). # All rights reserved. Contact: Pasi Eronen @@ -160,11 +161,14 @@ def document_main(request, name, rev=None): iesg_state_summary = doc.friendly_state() can_edit = has_role(request.user, ("Area Director", "Secretariat")) stream_slugs = StreamName.objects.values_list("slug", flat=True) - can_change_stream = bool(can_edit or ( - request.user.is_authenticated and - Role.objects.filter(name__in=("chair", "secr", "auth", "delegate"), - group__acronym__in=stream_slugs, - person__user=request.user))) + # For some reason, AnonymousUser has __iter__, but is not iterable, + # which causes problems in the filter() below. Work around this: + if request.user.is_authenticated: + roles = [ r for r in Role.objects.filter(group__acronym__in=stream_slugs, person__user=request.user) + if r.name.slug in r.group.type.features.matman_roles ] + else: + roles = [] + can_change_stream = bool(can_edit or roles) can_edit_iana_state = has_role(request.user, ("Secretariat", "IANA")) can_edit_replaces = has_role(request.user, ("Area Director", "Secretariat", "IRTF Chair", "WG Chair", "RG Chair", "WG Secretary", "RG Secretary")) diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py index 9ed357985..eb6ad7b39 100644 --- a/ietf/doc/views_draft.py +++ b/ietf/doc/views_draft.py @@ -1,3 +1,5 @@ +# Copyright The IETF Trust 2010-2019, All Rights Reserved + # changing state and metadata on Internet Drafts import datetime @@ -31,7 +33,7 @@ from ietf.doc.utils import ( add_state_change_event, can_adopt_draft, can_unadop set_replaces_for_document, default_consensus, tags_suffix, ) from ietf.doc.lastcall import request_last_call from ietf.doc.fields import SearchableDocAliasesField -from ietf.group.models import Group, Role +from ietf.group.models import Group, Role, GroupFeatures from ietf.iesg.models import TelechatDate from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream, user_is_person, is_individual_draft_author from ietf.ietfauth.utils import role_required @@ -1294,13 +1296,15 @@ def request_publication(request, name): ) class AdoptDraftForm(forms.Form): - group = forms.ModelChoiceField(queryset=Group.objects.filter(type__in=["wg", "rg"], state="active").order_by("-type", "acronym"), required=True, empty_label=None) + group = forms.ModelChoiceField(queryset=Group.objects.filter(type__features__acts_like_wg=True, state="active").order_by("-type", "acronym"), required=True, empty_label=None) newstate = forms.ModelChoiceField(queryset=State.objects.filter(type__in=['draft-stream-ietf','draft-stream-irtf'], used=True).exclude(slug__in=settings.GROUP_STATES_WITH_EXTRA_PROCESSING), required=True, label="State") comment = forms.CharField(widget=forms.Textarea, required=False, label="Comment", help_text="Optional comment explaining the reasons for the adoption.", strip=False) weeks = forms.IntegerField(required=False, label="Expected weeks in adoption state") def __init__(self, *args, **kwargs): user = kwargs.pop("user") + rg_features = GroupFeatures.objects.get(type_id='rg') + wg_features = GroupFeatures.objects.get(type_id='wg') super(AdoptDraftForm, self).__init__(*args, **kwargs) @@ -1308,17 +1312,21 @@ class AdoptDraftForm(forms.Form): if has_role(user, "Secretariat"): state_types.update(['draft-stream-ietf','draft-stream-irtf']) else: - if has_role(user, "IRTF Chair") or Group.objects.filter(type="rg", state="active", role__person__user=user, role__name__in=("chair", "delegate", "secr")).exists(): + if has_role(user, "IRTF Chair") or Group.objects.filter(type="rg", state="active", role__person__user=user, role__name__in=rg_features.matman_roles).exists(): state_types.add('draft-stream-irtf') - if Group.objects.filter(type="wg", state="active", role__person__user=user, role__name__in=("chair", "delegate", "secr")).exists(): + if Group.objects.filter(type="wg", state="active", role__person__user=user, role__name__in=wg_features.matman_roles).exists(): state_types.add('draft-stream-ietf') + + + + state_choices = State.objects.filter(type__in=state_types, used=True).exclude(slug__in=settings.GROUP_STATES_WITH_EXTRA_PROCESSING) if not has_role(user, "Secretariat"): if has_role(user, "IRTF Chair"): - group_queryset = self.fields["group"].queryset.filter(Q(role__person__user=user, role__name__in=("chair", "delegate", "secr"))|Q(type="rg", state="active")).distinct() + group_queryset = self.fields["group"].queryset.filter(Q(role__person__user=user, role__name__in=rg_features.matman_roles)|Q(type="rg", state="active")).distinct() else: - group_queryset = self.fields["group"].queryset.filter(role__person__user=user, role__name__in=("chair", "delegate", "secr")).distinct() + group_queryset = self.fields["group"].queryset.filter(role__person__user=user, role__name__in=wg_features.matman_roles).distinct() self.fields["group"].queryset = group_queryset self.fields['group'].choices = [(g.pk, '%s - %s' % (g.acronym, g.name)) for g in self.fields["group"].queryset] diff --git a/ietf/group/admin.py b/ietf/group/admin.py index 96e57895b..116beb05f 100644 --- a/ietf/group/admin.py +++ b/ietf/group/admin.py @@ -102,21 +102,30 @@ admin.site.register(Group, GroupAdmin) class GroupFeaturesAdmin(admin.ModelAdmin): list_display = [ + 'type', - 'customize_workflow', - 'has_chartering_process', - 'has_default_jabber', - 'has_dependencies', - 'has_documents', - 'has_nonsession_materials', 'has_milestones', + 'has_chartering_process', + 'has_documents', + 'has_dependencies', + 'has_session_materials', + 'has_nonsession_materials', + 'has_meetings', 'has_reviews', - 'material_types', + 'has_default_jabber', + 'acts_like_wg', + 'create_wiki', + 'custom_group_roles', + 'customize_workflow', + 'is_schedulable', + 'show_on_agenda', + 'req_subm_approval', 'agenda_type', + 'material_types', 'admin_roles', - 'about_page', - 'default_tab', - ] + 'matman_roles', + 'role_order', + ] admin.site.register(GroupFeatures, GroupFeaturesAdmin) class GroupHistoryAdmin(admin.ModelAdmin): diff --git a/ietf/group/migrations/0004_add_group_feature_fields.py b/ietf/group/migrations/0004_add_group_feature_fields.py new file mode 100644 index 000000000..6547e6405 --- /dev/null +++ b/ietf/group/migrations/0004_add_group_feature_fields.py @@ -0,0 +1,107 @@ +# Copyright The IETF Trust 2019, All Rights Reserved +# -*- coding: utf-8 -*- +# Generated by Django 1.11.16 on 2019-01-10 07:51 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('group', '0003_groupfeatures_data'), + ] + + operations = [ + migrations.AddField( + model_name='groupfeatures', + name='acts_like_wg', + field=models.BooleanField(default=False, verbose_name=b'WG-Like'), + ), + migrations.AddField( + model_name='groupfeatures', + name='create_wiki', + field=models.BooleanField(default=False, verbose_name=b'Wiki'), + ), + migrations.AddField( + model_name='groupfeatures', + name='custom_group_roles', + field=models.BooleanField(default=False, verbose_name=b'Group Roles'), + ), + migrations.AddField( + model_name='groupfeatures', + name='has_session_materials', + field=models.BooleanField(default=False, verbose_name=b'Materials'), + ), + migrations.AddField( + model_name='groupfeatures', + name='is_schedulable', + field=models.BooleanField(default=False, verbose_name=b'Schedulable'), + ), + migrations.AddField( + model_name='groupfeatures', + name='role_order', + field=models.CharField(default=b'chair,secr,member', help_text=b'The order in which roles are shown, for instance on photo pages', max_length=128, validators=[django.core.validators.RegexValidator(code=b'invalid', message=b'Enter a comma-separated list of role slugs', regex=b'[a-z0-9_-]+(,[a-z0-9_-]+)*')]), + ), + migrations.AddField( + model_name='groupfeatures', + name='show_on_agenda', + field=models.BooleanField(default=False, verbose_name=b'On Agenda'), + ), + migrations.AddField( + model_name='groupfeatures', + name='req_subm_approval', + field=models.BooleanField(default=False, verbose_name=b'Subm. Approval'), + ), + migrations.AddField( + model_name='groupfeatures', + name='matman_roles', + field=models.CharField(default=b'ad,chair,delegate,secr', max_length=64, validators=[django.core.validators.RegexValidator(code=b'invalid', message=b'Enter a comma-separated list of role slugs', regex=b'[a-z0-9_-]+(,[a-z0-9_-]+)*')]), + ), + migrations.AddField( + model_name='historicalgroupfeatures', + name='acts_like_wg', + field=models.BooleanField(default=False, verbose_name=b'WG-Like'), + ), + migrations.AddField( + model_name='historicalgroupfeatures', + name='create_wiki', + field=models.BooleanField(default=False, verbose_name=b'Wiki'), + ), + migrations.AddField( + model_name='historicalgroupfeatures', + name='custom_group_roles', + field=models.BooleanField(default=False, verbose_name=b'Group Roles'), + ), + migrations.AddField( + model_name='historicalgroupfeatures', + name='has_session_materials', + field=models.BooleanField(default=False, verbose_name=b'Materials'), + ), + migrations.AddField( + model_name='historicalgroupfeatures', + name='is_schedulable', + field=models.BooleanField(default=False, verbose_name=b'Schedulable'), + ), + migrations.AddField( + model_name='historicalgroupfeatures', + name='role_order', + field=models.CharField(default=b'chair,secr,member', help_text=b'The order in which roles are shown, for instance on photo pages', max_length=128, validators=[django.core.validators.RegexValidator(code=b'invalid', message=b'Enter a comma-separated list of role slugs', regex=b'[a-z0-9_-]+(,[a-z0-9_-]+)*')]), + ), + migrations.AddField( + model_name='historicalgroupfeatures', + name='show_on_agenda', + field=models.BooleanField(default=False, verbose_name=b'On Agenda'), + ), + migrations.AddField( + model_name='historicalgroupfeatures', + name='req_subm_approval', + field=models.BooleanField(default=False, verbose_name=b'Subm. Approval'), + ), + migrations.AddField( + model_name='historicalgroupfeatures', + name='matman_roles', + field=models.CharField(default=b'ad,chair,delegate,secr', max_length=64, validators=[django.core.validators.RegexValidator(code=b'invalid', message=b'Enter a comma-separated list of role slugs', regex=b'[a-z0-9_-]+(,[a-z0-9_-]+)*')]), + ), + ] diff --git a/ietf/group/migrations/0004_auto_20181218_2349.py b/ietf/group/migrations/0004_auto_20181218_2349.py deleted file mode 100644 index 1118d7eff..000000000 --- a/ietf/group/migrations/0004_auto_20181218_2349.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.17 on 2018-12-18 23:49 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('group', '0003_groupfeatures_data'), - ] - - operations = [ - migrations.AlterField( - model_name='historicalgroupfeatures', - name='agenda_type', - field=models.ForeignKey(blank=True, db_constraint=False, default=b'ietf', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='name.AgendaTypeName'), - ), - ] diff --git a/ietf/group/migrations/0005_group_features_list_data_to_json.py b/ietf/group/migrations/0005_group_features_list_data_to_json.py new file mode 100644 index 000000000..67e6f9512 --- /dev/null +++ b/ietf/group/migrations/0005_group_features_list_data_to_json.py @@ -0,0 +1,49 @@ +# Copyright The IETF Trust 2019, All Rights Reserved +# -*- coding: utf-8 -*- +# Generated by Django 1.11.16 on 2019-01-09 09:02 +from __future__ import unicode_literals + +import json +import re + +from django.db import migrations + +import debug # pyflakes:ignore + +def forward(apps, schema_editor): + GroupFeatures = apps.get_model('group', 'GroupFeatures') + for f in GroupFeatures.objects.all(): + for a in ['material_types', 'admin_roles', 'matman_roles', 'role_order']: + v = getattr(f, a, None) + if v != None: + v = re.sub(r'[][\\"\' ]+', '', v) + v = v.split(',') + v = json.dumps(v) + setattr(f, a, v) + f.save() +# This migration changes existing data fields in an incompatible manner, and +# would not be interleavable if we hadn't added compatibility code in +# Group.features() beforehand. With that patched in, we permit interleaving. +forward.interleavable = True + +def reverse(apps, schema_editor): + GroupFeatures = apps.get_model('group', 'GroupFeatures') + for f in GroupFeatures.objects.all(): + for a in ['material_types', 'admin_roles', 'matman_roles', 'role_order']: + v = getattr(f, a, None) + if v != None: + v = getattr(f, a) + v = json.loads(v) + v = ','.join(v) + setattr(f, a, v) + f.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('group', '0004_add_group_feature_fields'), + ] + + operations = [ + migrations.RunPython(forward, reverse), + ] diff --git a/ietf/group/migrations/0006_group_features_lists_to_jsonfield.py b/ietf/group/migrations/0006_group_features_lists_to_jsonfield.py new file mode 100644 index 000000000..b3233a225 --- /dev/null +++ b/ietf/group/migrations/0006_group_features_lists_to_jsonfield.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.16 on 2019-01-16 05:53 +from __future__ import unicode_literals + +from django.db import migrations, models +import jsonfield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('group', '0005_group_features_list_data_to_json'), + ] + + operations = [ + migrations.AlterField( + model_name='groupfeatures', + name='admin_roles', + field=jsonfield.fields.JSONField(default=b'chair', max_length=64), + ), + migrations.AlterField( + model_name='groupfeatures', + name='custom_group_roles', + field=models.BooleanField(default=False, verbose_name=b'Cust. Roles'), + ), + migrations.AlterField( + model_name='groupfeatures', + name='has_nonsession_materials', + field=models.BooleanField(default=False, verbose_name=b'Other Matrl.'), + ), + migrations.AlterField( + model_name='groupfeatures', + name='has_session_materials', + field=models.BooleanField(default=False, verbose_name=b'Sess Matrl.'), + ), + migrations.AlterField( + model_name='groupfeatures', + name='material_types', + field=jsonfield.fields.JSONField(default=b'slides', max_length=64), + ), + migrations.AlterField( + model_name='groupfeatures', + name='matman_roles', + field=jsonfield.fields.JSONField(default=b'ad,chair,delegate,secr', max_length=128), + ), + migrations.AlterField( + model_name='groupfeatures', + name='role_order', + field=jsonfield.fields.JSONField(default=b'chair,secr,member', help_text=b'The order in which roles are shown, for instance on photo pages. Enter valid JSON.', max_length=128), + ), + migrations.AlterField( + model_name='historicalgroupfeatures', + name='admin_roles', + field=jsonfield.fields.JSONField(default=b'chair', max_length=64), + ), + migrations.AlterField( + model_name='historicalgroupfeatures', + name='custom_group_roles', + field=models.BooleanField(default=False, verbose_name=b'Cust. Roles'), + ), + migrations.AlterField( + model_name='historicalgroupfeatures', + name='has_nonsession_materials', + field=models.BooleanField(default=False, verbose_name=b'Other Matrl.'), + ), + migrations.AlterField( + model_name='historicalgroupfeatures', + name='has_session_materials', + field=models.BooleanField(default=False, verbose_name=b'Sess Matrl.'), + ), + migrations.AlterField( + model_name='historicalgroupfeatures', + name='material_types', + field=jsonfield.fields.JSONField(default=b'slides', max_length=64), + ), + migrations.AlterField( + model_name='historicalgroupfeatures', + name='matman_roles', + field=jsonfield.fields.JSONField(default=b'ad,chair,delegate,secr', max_length=128), + ), + migrations.AlterField( + model_name='historicalgroupfeatures', + name='role_order', + field=jsonfield.fields.JSONField(default=b'chair,secr,member', help_text=b'The order in which roles are shown, for instance on photo pages. Enter valid JSON.', max_length=128), + ), + ] diff --git a/ietf/group/migrations/0007_new_group_features_data.py b/ietf/group/migrations/0007_new_group_features_data.py new file mode 100644 index 000000000..6ff081e77 --- /dev/null +++ b/ietf/group/migrations/0007_new_group_features_data.py @@ -0,0 +1,211 @@ +# Copyright The IETF Trust 2019, All Rights Reserved +# -*- coding: utf-8 -*- +# Generated by Django 1.11.16 on 2019-01-09 09:02 +from __future__ import unicode_literals + + +from django.db import migrations + +import debug # pyflakes:ignore + +group_type_features = { + u'ag': { + 'custom_group_roles': True, + 'has_session_materials': True, + 'acts_like_wg': True, + 'create_wiki': True, + 'is_schedulable': True, + 'req_subm_approval': True, + 'show_on_agenda': True, + 'matman_roles': ['ad', 'chair', 'delegate', 'secr'], + 'role_order': ['chair', 'secr'], + }, + u'area': { + 'custom_group_roles': True, + 'has_session_materials': False, + 'acts_like_wg': False, + 'create_wiki': True, + 'is_schedulable': False, + 'req_subm_approval': True, + 'show_on_agenda': False, + 'matman_roles': ['ad', 'chair', 'delegate', 'secr'], + 'role_order': ['chair', 'secr'], + }, + u'dir': { + 'custom_group_roles': True, + 'has_session_materials': False, + 'acts_like_wg': False, + 'create_wiki': True, + 'is_schedulable': False, + 'req_subm_approval': True, + 'show_on_agenda': False, + 'matman_roles': ['ad', 'chair', 'delegate', 'secr'], + 'role_order': ['chair', 'secr'], + }, + u'review': { + 'custom_group_roles': True, + 'has_session_materials': False, + 'acts_like_wg': False, + 'create_wiki': True, + 'is_schedulable': False, + 'req_subm_approval': True, + 'show_on_agenda': False, + 'matman_roles': ['ad', 'secr'], + 'role_order': ['chair', 'secr'], + }, + u'iab': { + 'custom_group_roles': True, + 'has_session_materials': False, + 'acts_like_wg': False, + 'create_wiki': False, + 'is_schedulable': False, + 'req_subm_approval': True, + 'show_on_agenda': True, + 'matman_roles': [], + 'role_order': ['chair', 'secr'], + }, + u'ietf': { + 'custom_group_roles': True, + 'has_session_materials': True, + 'acts_like_wg': False, + 'create_wiki': False, + 'is_schedulable': False, + 'req_subm_approval': True, + 'show_on_agenda': False, + 'matman_roles': ['chair', 'delegate'], + 'role_order': ['chair', 'secr'], + }, + u'individ': { + 'custom_group_roles': True, + 'has_session_materials': False, + 'acts_like_wg': False, + 'create_wiki': False, + 'is_schedulable': False, + 'req_subm_approval': False, + 'show_on_agenda': False, + 'matman_roles': ['auth'], + 'role_order': ['chair', 'secr'], + }, + u'irtf': { + 'custom_group_roles': True, + 'has_session_materials': False, + 'acts_like_wg': False, + 'create_wiki': False, + 'is_schedulable': False, + 'req_subm_approval': True, + 'show_on_agenda': False, + 'matman_roles': ['chair', 'delegate', 'secr'], + 'role_order': ['chair', 'secr'], + }, + u'isoc': { + 'custom_group_roles': True, + 'has_session_materials': False, + 'acts_like_wg': False, + 'create_wiki': False, + 'is_schedulable': False, + 'req_subm_approval': True, + 'show_on_agenda': False, + 'matman_roles': ['chair', 'secr'], + 'role_order': ['chair', 'secr'], + }, + u'nomcom': { + 'custom_group_roles': True, + 'has_session_materials': False, + 'acts_like_wg': False, + 'create_wiki': True, + 'is_schedulable': False, + 'req_subm_approval': True, + 'show_on_agenda': False, + 'matman_roles': ['chair'], + 'role_order': ['chair', 'member', 'advisor'], + }, + u'program': { + 'custom_group_roles': True, + 'has_session_materials': False, + 'acts_like_wg': False, + 'create_wiki': False, + 'is_schedulable': False, + 'req_subm_approval': False, + 'show_on_agenda': False, + 'matman_roles': ['chair', 'secr'], + 'role_order': ['chair', 'secr'], + }, + u'rfcedtyp': { + 'custom_group_roles': True, + 'has_session_materials': False, + 'acts_like_wg': False, + 'create_wiki': False, + 'is_schedulable': False, + 'req_subm_approval': True, + 'show_on_agenda': False, + 'matman_roles': ['chair', 'secr'], + 'role_order': ['chair', 'secr'], + }, + u'rg': { + 'custom_group_roles': False, + 'has_session_materials': True, + 'acts_like_wg': True, + 'create_wiki': True, + 'is_schedulable': True, + 'req_subm_approval': True, + 'show_on_agenda': True, + 'matman_roles': ['chair', 'secr'], + 'role_order': ['chair', 'secr'], + }, + u'sdo': { + 'custom_group_roles': True, + 'has_session_materials': False, + 'acts_like_wg': False, + 'create_wiki': False, + 'is_schedulable': False, + 'req_subm_approval': True, + 'show_on_agenda': False, + 'matman_roles': ['liaiman', 'matman'], + 'role_order': ['liaiman'], + }, + u'team': { + 'custom_group_roles': True, + 'has_session_materials': False, + 'acts_like_wg': False, + 'create_wiki': True, + 'is_schedulable': False, + 'req_subm_approval': False, + 'show_on_agenda': False, + 'matman_roles': ['chair', 'matman'], + 'role_order': ['chair', 'member', 'matman'], + }, + u'wg': { + 'custom_group_roles': False, + 'has_session_materials': True, + 'acts_like_wg': True, + 'create_wiki': True, + 'is_schedulable': True, + 'req_subm_approval': True, + 'show_on_agenda': True, + 'matman_roles': ['ad', 'chair', 'delegate', 'secr'], + 'role_order': ['chair', 'secr', 'delegate'], + }, +} + +def forward(apps, schema_editor): + GroupFeatures = apps.get_model('group', 'GroupFeatures') + for type in group_type_features: + features = group_type_features[type] + gf = GroupFeatures.objects.get(type=type) + for k,v in features.items(): + setattr(gf, k, v) + gf.save() +forward.interleavable = True + +def reverse(apps, schema_editor): + pass + +class Migration(migrations.Migration): + + dependencies = [ + ('group', '0006_group_features_lists_to_jsonfield') + ] + + operations = [ + migrations.RunPython(forward, reverse), + ] diff --git a/ietf/group/migrations/0008_group_features_onetoone.py b/ietf/group/migrations/0008_group_features_onetoone.py new file mode 100644 index 000000000..770f3a3ba --- /dev/null +++ b/ietf/group/migrations/0008_group_features_onetoone.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.16 on 2019-01-19 10:08 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import ietf.utils.models + +class Migration(migrations.Migration): + + dependencies = [ + ('group', '0007_new_group_features_data'), + ] + + operations = [ + migrations.AlterField( + model_name='groupfeatures', + name='type', + field=ietf.utils.models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='features', serialize=False, to='name.GroupTypeName'), + ), + migrations.AlterField( + model_name='historicalgroupfeatures', + name='type', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='name.GroupTypeName'), + ), + ] diff --git a/ietf/group/models.py b/ietf/group/models.py index 0197f34cf..6bdb07423 100644 --- a/ietf/group/models.py +++ b/ietf/group/models.py @@ -2,6 +2,7 @@ import datetime import email.utils +import jsonfield import os import re from urlparse import urljoin @@ -40,31 +41,20 @@ class GroupInfo(models.Model): def __unicode__(self): return self.name - def name_with_acronym(self): - res = self.name - if self.type_id in ("wg", "rg", "ag", "area"): - res += " %s (%s)" % (self.type, self.acronym) - return res - def ad_role(self): return self.role_set.filter(name='ad').first() @property def features(self): if not hasattr(self, "features_cache"): - features = GroupFeatures.objects.get(type=self.type) - # convert textual lists to python lists: - for a in ['material_types', 'admin_roles', ]: - v = getattr(features, a) - setattr(features, a, v.split(',')) - self.features_cache = features + self.features_cache = GroupFeatures.objects.get(type=self.type) return self.features_cache def about_url(self): # bridge gap between group-type prefixed URLs and /group/ ones from django.urls import reverse as urlreverse kwargs = { 'acronym': self.acronym } - if self.type_id in ("wg", "rg", "ag"): + if self.features.acts_like_wg: kwargs["group_type"] = self.type_id return urlreverse(self.features.about_page, kwargs=kwargs) @@ -214,25 +204,35 @@ validate_comma_separated_roles = RegexValidator( ) class GroupFeatures(models.Model): - type = ForeignKey(GroupTypeName, primary_key=True, null=False, related_name='features') + type = OneToOneField(GroupTypeName, primary_key=True, null=False, related_name='features') history = HistoricalRecords() # has_milestones = models.BooleanField("Milestones", default=False) has_chartering_process = models.BooleanField("Chartering", default=False) has_documents = models.BooleanField("Documents", default=False) # i.e. drafts/RFCs has_dependencies = models.BooleanField("Dependencies",default=False) # Do dependency graphs for group documents make sense? - has_nonsession_materials= models.BooleanField("Materials", default=False) + has_session_materials = models.BooleanField("Sess Matrl.", default=False) + has_nonsession_materials= models.BooleanField("Other Matrl.", default=False) has_meetings = models.BooleanField("Meetings", default=False) has_reviews = models.BooleanField("Reviews", default=False) has_default_jabber = models.BooleanField("Jabber", default=False) + # + acts_like_wg = models.BooleanField("WG-Like", default=False) + create_wiki = models.BooleanField("Wiki", default=False) + custom_group_roles = models.BooleanField("Cust. Roles",default=False) customize_workflow = models.BooleanField("Workflow", default=False) + is_schedulable = models.BooleanField("Schedulable",default=False) + show_on_agenda = models.BooleanField("On Agenda", default=False) + req_subm_approval = models.BooleanField("Subm. Approval", default=False) + # agenda_type = models.ForeignKey(AgendaTypeName, null=True, default="ietf", on_delete=CASCADE) about_page = models.CharField(max_length=64, blank=False, default="ietf.group.views.group_about" ) default_tab = models.CharField(max_length=64, blank=False, default="ietf.group.views.group_about" ) - material_types = models.CharField(max_length=64, blank=False, default="slides", - validators=[validate_comma_separated_materials]) - admin_roles = models.CharField(max_length=64, blank=False, default="chair", - validators=[validate_comma_separated_roles]) + material_types = jsonfield.JSONField(max_length=64, blank=False, default="slides") + admin_roles = jsonfield.JSONField(max_length=64, blank=False, default="chair") + 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.") class GroupHistory(GroupInfo): diff --git a/ietf/group/utils.py b/ietf/group/utils.py index 5615a2280..f72c51ff8 100644 --- a/ietf/group/utils.py +++ b/ietf/group/utils.py @@ -121,12 +121,12 @@ def milestone_reviewer_for_group_type(group_type): return "Area Director" def can_manage_materials(user, group): - return has_role(user, 'Secretariat') or group.has_role(user, ("chair", "delegate", "secr", "matman", "ad")) + return has_role(user, 'Secretariat') or group.has_role(user, group.features.matman_roles) def can_provide_status_update(user, group): - if not group.type_id in ['wg','rg','ag','team']: + if not group.features.acts_like_wg: return False - return has_role(user, 'Secretariat') or group.has_role(user, ("chair", "delegate", "secr", "ad",)) + return has_role(user, 'Secretariat') or group.has_role(user, group.features.matman_roles) def get_group_or_404(acronym, group_type): """Helper to overcome the schism between group-type prefixed URLs and generic.""" diff --git a/ietf/group/views.py b/ietf/group/views.py index aab40f66d..9a9085044 100644 --- a/ietf/group/views.py +++ b/ietf/group/views.py @@ -1,4 +1,5 @@ -# Copyright The IETF Trust 2007, All Rights Reserved +# Copyright The IETF Trust 2007-2019, All Rights Reserved + # Portion Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). # All rights reserved. Contact: Pasi Eronen @@ -69,7 +70,8 @@ from ietf.group.forms import (GroupForm, StatusUpdateForm, ConcludeGroupForm, St ManageReviewRequestForm, EmailOpenAssignmentsForm, ReviewerSettingsForm, AddUnavailablePeriodForm, EndUnavailablePeriodForm, ReviewSecretarySettingsForm, ) from ietf.group.mails import email_admin_re_charter, email_personnel_change, email_comment -from ietf.group.models import ( Group, Role, GroupEvent, GroupStateTransitions, GroupURL, ChangeStateGroupEvent ) +from ietf.group.models import ( Group, Role, GroupEvent, GroupStateTransitions, GroupURL, + ChangeStateGroupEvent, GroupFeatures ) from ietf.group.utils import (get_charter_text, can_manage_group_type, milestone_reviewer_for_group_type, can_provide_status_update, can_manage_materials, @@ -385,7 +387,8 @@ def bofs(request, group_type): def chartering_groups(request): charter_states = State.objects.filter(used=True, type="charter").exclude(slug__in=("approved", "notrev")) - group_types = GroupTypeName.objects.filter(slug__in=("wg", "rg")) + group_type_slugs = [ f.type.slug for f in GroupFeatures.objects.filter(has_chartering_process=True) ] + group_types = GroupTypeName.objects.filter(slug__in=group_type_slugs) for t in group_types: t.chartering_groups = Group.objects.filter(type=t, charter__states__in=charter_states).select_related("state", "charter").order_by("acronym") @@ -788,16 +791,7 @@ def group_photos(request, group_type=None, acronym=None): group = get_object_or_404(Group, acronym=acronym) roles = sorted(Role.objects.filter(group__acronym=acronym),key=lambda x: x.name.name+x.person.last_name()) - if group.type_id in ['wg', 'rg', 'ag', ]: - roles = reorder_roles(roles, ['chair', 'secr']) - elif group.type_id in ['nomcom', ]: - roles = reorder_roles(roles, ['chair', 'member', 'advisor', ]) - elif group.type_id in ['team', ]: - roles = reorder_roles(roles, ['chair', 'member', 'matman', ]) - elif group.type_id in ['sdo', ]: - roles = reorder_roles(roles, ['liaiman', ]) - else: - pass + roles = reorder_roles(roles, group.features.role_order) for role in roles: role.last_initial = role.person.last_name()[0] return render(request, 'group/group_photos.html', @@ -1197,6 +1191,7 @@ def stream_documents(request, acronym): docs, meta = prepare_document_table(request, qs, max_results=1000) return render(request, 'group/stream_documents.html', {'stream':stream, 'docs':docs, 'meta':meta, 'editable':editable } ) + def stream_edit(request, acronym): group = get_object_or_404(Group, acronym=acronym) @@ -1252,7 +1247,7 @@ def group_json(request, acronym): @cache_control(public=True, max_age=30*60) @cache_page(30 * 60) def group_menu_data(request): - groups = Group.objects.filter(state="active", type__in=("wg", "rg"), parent__state="active").order_by("acronym") + groups = Group.objects.filter(state="active", type__features__acts_like_wg=True, parent__state="active").order_by("acronym") groups_by_parent = defaultdict(list) for g in groups: diff --git a/ietf/ietfauth/utils.py b/ietf/ietfauth/utils.py index 20d820d57..d317dcd9e 100644 --- a/ietf/ietfauth/utils.py +++ b/ietf/ietfauth/utils.py @@ -1,3 +1,5 @@ +# Copyright The IETF Trust 2013-2019, All Rights Reserved + # various authentication and authorization utilities from functools import wraps @@ -11,7 +13,7 @@ from django.utils.decorators import available_attrs import debug # pyflakes:ignore -from ietf.group.models import Role +from ietf.group.models import Group, Role from ietf.person.models import Person def user_is_person(user, person): @@ -134,16 +136,20 @@ def is_authorized_in_doc_stream(user, doc): if doc.stream.slug == "ietf" and doc.group.type_id == "individ": return False + matman_roles = doc.group.features.matman_roles if doc.stream.slug == "ietf": group_req = Q(group=doc.group) elif doc.stream.slug == "irtf": group_req = Q(group__acronym=doc.stream.slug) | Q(group=doc.group) elif doc.stream.slug in ("iab", "ise"): + if doc.group.type.slug == 'individ': + # A lot of special cases here, for stream slugs and group acronyms + matman_roles = Group.objects.get(acronym=doc.stream.slug).features.matman_roles group_req = Q(group__acronym=doc.stream.slug) else: group_req = Q() - return Role.objects.filter(Q(name__in=("chair", "secr", "delegate", "auth"), person__user=user) & group_req).exists() + return Role.objects.filter(Q(name__in=matman_roles, person__user=user) & group_req).exists() def is_authorized_in_group(user, group): """Return whether user is authorized to perform duties on @@ -163,7 +169,7 @@ def is_authorized_in_group(user, group): if group.parent.acronym == 'iab' and has_role(user, ['IAB','IAB Executive Director',]): return True - return Role.objects.filter(name__in=("chair", "secr", "delegate", "auth"), person__user=user,group=group ).exists() + return Role.objects.filter(name__in=group.features.matman_roles, person__user=user,group=group ).exists() def is_individual_draft_author(user, doc): diff --git a/ietf/mailinglists/views.py b/ietf/mailinglists/views.py index bd4126443..ab3cc559c 100644 --- a/ietf/mailinglists/views.py +++ b/ietf/mailinglists/views.py @@ -11,7 +11,7 @@ from ietf.group.models import Group from ietf.mailinglists.models import List def groups(request): - groups = Group.objects.filter(type__in=("wg", "rg", "ag"), list_archive__startswith='http').exclude(state__in=('bof', 'conclude')).order_by("acronym") + groups = Group.objects.filter(type__features__acts_like_wg=True, list_archive__startswith='http').exclude(state__in=('bof', 'conclude')).order_by("acronym") return render(request, "mailinglists/group_archives.html", { "groups": groups } ) @@ -19,7 +19,7 @@ def groups(request): # safely cache this for some time. @cache_page(15*60) def nonwg(request): - groups = Group.objects.filter(type__in=("wg", "rg")).exclude(state__in=['bof', 'conclude']).order_by("acronym") + groups = Group.objects.filter(type__features__acts_like_wg=True).exclude(state__in=['bof', 'conclude']).order_by("acronym") #urls = [ g.list_archive for g in groups if '.ietf.org' in g.list_archive ] diff --git a/ietf/mailtrigger/models.py b/ietf/mailtrigger/models.py index f6e6a0c1e..a0e9ce701 100644 --- a/ietf/mailtrigger/models.py +++ b/ietf/mailtrigger/models.py @@ -68,7 +68,7 @@ class Recipient(models.Model): addrs = [] if 'doc' in kwargs: doc=kwargs['doc'] - if doc.group and doc.group.type.slug in ['wg','rg','ag',]: + if doc.group and doc.group.features.acts_like_wg: addrs.append('%s-chairs@ietf.org'%doc.group.acronym) return addrs @@ -76,7 +76,7 @@ class Recipient(models.Model): addrs = [] if 'doc' in kwargs: doc=kwargs['doc'] - if doc.group and doc.group.type.slug in ['wg','rg','ag',]: + if doc.group and doc.group.features.acts_like_wg: addrs.extend(doc.group.role_set.filter(name='delegate').values_list('email__address',flat=True)) return addrs @@ -84,7 +84,7 @@ class Recipient(models.Model): addrs = [] if 'doc' in kwargs: doc=kwargs['doc'] - if doc.group.type.slug in ['wg','rg','ag',]: + if doc.group.features.acts_like_wg: if doc.group.list_email: addrs.append(doc.group.list_email) return addrs @@ -226,7 +226,7 @@ class Recipient(models.Model): new_author_email_set = set(author["email"] for author in submission.authors if author.get("email")) if doc.group and old_author_email_set != new_author_email_set: - if doc.group.type_id in ['wg','rg','ag']: + if doc.group.features.acts_like_wg: addrs.extend(Recipient.objects.get(slug='group_chairs').gather(**{'group':doc.group})) elif doc.group.type_id in ['area']: addrs.extend(Recipient.objects.get(slug='group_responsible_directors').gather(**{'group':doc.group})) diff --git a/ietf/mailtrigger/utils.py b/ietf/mailtrigger/utils.py index 14a2c3d1a..a35dc3295 100644 --- a/ietf/mailtrigger/utils.py +++ b/ietf/mailtrigger/utils.py @@ -67,9 +67,9 @@ def gather_relevant_expansions(**kwargs): relevant.update(starts_with('group_')) relevant.update(starts_with('milestones_')) group = kwargs['group'] - if group.type_id in ['rg','wg','ag',]: + if group.features.acts_like_wg: relevant.update(starts_with('session_')) - if group.type_id in ['wg',]: + if group.features.has_chartering_process: relevant.update(['charter_external_review',]) if 'submission' in kwargs: diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 6ae54e51b..4470e2fb0 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -1,3 +1,4 @@ +# Copyright The IETF Trust 2009-2019, All Rights Reserved # -*- coding: utf-8 -*- import json @@ -294,14 +295,16 @@ class MeetingTests(TestCase): self.assertTrue("1. WG status" in unicontent(r)) # session minutes - r = self.client.get(urlreverse("ietf.meeting.views.materials_document", - kwargs=dict(num=meeting.number, document=session.minutes()))) + url = urlreverse("ietf.meeting.views.materials_document", + kwargs=dict(num=meeting.number, document=session.minutes())) + r = self.client.get(url) self.assertEqual(r.status_code, 200) self.assertTrue("1. More work items underway" in unicontent(r)) # test with explicit meeting number in url if meeting.number.isdigit(): - r = self.client.get(urlreverse("ietf.meeting.views.materials", kwargs=dict(num=meeting.number))) + url = urlreverse("ietf.meeting.views.materials", kwargs=dict(num=meeting.number)) + r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) row = q('#content #%s' % str(session.group.acronym)).closest("tr") @@ -311,7 +314,8 @@ class MeetingTests(TestCase): self.assertFalse(row.find("a:contains(\"Bad Slideshow\")")) # test with no meeting number in url - r = self.client.get(urlreverse("ietf.meeting.views.materials", kwargs=dict())) + url = urlreverse("ietf.meeting.views.materials", kwargs=dict()) + r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) row = q('#content #%s' % str(session.group.acronym)).closest("tr") @@ -322,7 +326,8 @@ class MeetingTests(TestCase): # test with a loggged-in wg chair self.client.login(username="marschairman", password="marschairman+password") - r = self.client.get(urlreverse("ietf.meeting.views.materials", kwargs=dict(num=meeting.number))) + url = urlreverse("ietf.meeting.views.materials", kwargs=dict(num=meeting.number)) + r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) row = q('#content #%s' % str(session.group.acronym)).closest("tr") diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 50aeec809..14f08e01b 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -1264,8 +1264,8 @@ }, { "fields": { - "desc": "This is the initial state when an AD proposes a new charter. The normal next state is Internal review if the idea is accepted, or Not currently under review if the idea is abandoned.", - "name": "Informal IESG review", + "desc": "The proposed charter is not being considered at this time. A proposed charter will remain in this state until an AD moves it to Start Chartering/Rechartering (Internal IESG/IAB Review). This state is useful for drafting the charter, discussing with chairs, etc.", + "name": "Draft Charter", "next_states": [], "order": 0, "slug": "infrev", @@ -1277,8 +1277,8 @@ }, { "fields": { - "desc": "The IESG and IAB are reviewing the early draft of the charter; this is the initial IESG and IAB review. The usual next state is External review if the idea is adopted, or Informal IESG review if the IESG decides the idea needs more work, or Not currently under review if the idea is abandoned", - "name": "Internal review", + "desc": "This is the state when you'd like to propose the charter / new charter. This state also allows you to ask whether external review can be skipped in ballot. After you select this state, the Secretariat takes over and drives the rest of the process.", + "name": "Start Chartering/Rechartering (Internal IESG/IAB Review)", "next_states": [], "order": 0, "slug": "intrev", @@ -1290,8 +1290,8 @@ }, { "fields": { - "desc": "The IETF community and possibly other standards development organizations (SDOs) are reviewing the proposed charter. The usual next state is IESG review, although it might move to Not currently under review if the idea is abandoned during the external review.", - "name": "External review", + "desc": "This state is selected by the Secretariat (AD's, keep yer grubby mits off this!) when it has been decided that the charter needs external review.", + "name": "External Review (Message to Community, Selected by Secretariat)", "next_states": [], "order": 0, "slug": "extrev", @@ -1303,8 +1303,8 @@ }, { "fields": { - "desc": "The IESG is reviewing the discussion from the external review of the proposed charter. The usual next state is Approved, or Not currently under review if the idea is abandoned.", - "name": "IESG review", + "desc": "This state is selected by the Secretariat (AD's, keep yer grubby mits off this!) when the IESG is reviewing the discussion from the external review of the proposed charter (this is similar to the IESG Evaluation state for a draft).", + "name": "IESG Review (Charter for Approval, Selected by Secretariat)", "next_states": [], "order": 0, "slug": "iesgrev", @@ -2181,7 +2181,7 @@ "used": true }, "model": "doc.state", - "pk": 152 + "pk": 150 }, { "fields": { @@ -2347,8 +2347,11 @@ { "fields": { "about_page": "ietf.group.views.group_about", - "admin_roles": "chair", + "acts_like_wg": true, + "admin_roles": "[\"chair\"]", "agenda_type": "ietf", + "create_wiki": true, + "custom_group_roles": true, "customize_workflow": false, "default_tab": "ietf.group.views.group_about", "has_chartering_process": false, @@ -2359,7 +2362,13 @@ "has_milestones": false, "has_nonsession_materials": false, "has_reviews": false, - "material_types": "slides" + "has_session_materials": true, + "is_schedulable": true, + "material_types": "[\"slides\"]", + "matman_roles": "[\"ad\",\"chair\",\"delegate\",\"secr\"]", + "req_subm_approval": true, + "role_order": "[\"chair\",\"secr\"]", + "show_on_agenda": true }, "model": "group.groupfeatures", "pk": "ag" @@ -2367,8 +2376,11 @@ { "fields": { "about_page": "ietf.group.views.group_about", - "admin_roles": "ad", + "acts_like_wg": false, + "admin_roles": "[\"ad\"]", "agenda_type": "ietf", + "create_wiki": true, + "custom_group_roles": true, "customize_workflow": false, "default_tab": "ietf.group.views.group_about", "has_chartering_process": false, @@ -2379,7 +2391,13 @@ "has_milestones": false, "has_nonsession_materials": false, "has_reviews": false, - "material_types": "slides" + "has_session_materials": false, + "is_schedulable": false, + "material_types": "[\"slides\"]", + "matman_roles": "[\"ad\",\"chair\",\"delegate\",\"secr\"]", + "req_subm_approval": true, + "role_order": "[\"chair\",\"secr\"]", + "show_on_agenda": false }, "model": "group.groupfeatures", "pk": "area" @@ -2387,8 +2405,11 @@ { "fields": { "about_page": "ietf.group.views.group_about", - "admin_roles": "chair,secr", + "acts_like_wg": false, + "admin_roles": "[\"chair\",\"secr\"]", "agenda_type": null, + "create_wiki": true, + "custom_group_roles": true, "customize_workflow": false, "default_tab": "ietf.group.views.group_about", "has_chartering_process": false, @@ -2399,7 +2420,13 @@ "has_milestones": false, "has_nonsession_materials": false, "has_reviews": false, - "material_types": "slides" + "has_session_materials": false, + "is_schedulable": false, + "material_types": "[\"slides\"]", + "matman_roles": "[\"ad\",\"chair\",\"delegate\",\"secr\"]", + "req_subm_approval": true, + "role_order": "[\"chair\",\"secr\"]", + "show_on_agenda": false }, "model": "group.groupfeatures", "pk": "dir" @@ -2407,8 +2434,11 @@ { "fields": { "about_page": "ietf.group.views.group_about", - "admin_roles": "chair", + "acts_like_wg": false, + "admin_roles": "[\"chair\"]", "agenda_type": "ietf", + "create_wiki": false, + "custom_group_roles": true, "customize_workflow": false, "default_tab": "ietf.group.views.group_about", "has_chartering_process": false, @@ -2419,7 +2449,13 @@ "has_milestones": false, "has_nonsession_materials": false, "has_reviews": false, - "material_types": "slides" + "has_session_materials": false, + "is_schedulable": false, + "material_types": "[\"slides\"]", + "matman_roles": "[]", + "req_subm_approval": true, + "role_order": "[\"chair\",\"secr\"]", + "show_on_agenda": true }, "model": "group.groupfeatures", "pk": "iab" @@ -2427,8 +2463,11 @@ { "fields": { "about_page": "ietf.group.views.group_about", - "admin_roles": "chair,lead", + "acts_like_wg": false, + "admin_roles": "[\"chair\",\"lead\"]", "agenda_type": "ietf", + "create_wiki": false, + "custom_group_roles": true, "customize_workflow": false, "default_tab": "ietf.group.views.group_about", "has_chartering_process": false, @@ -2439,7 +2478,13 @@ "has_milestones": false, "has_nonsession_materials": false, "has_reviews": false, - "material_types": "slides" + "has_session_materials": true, + "is_schedulable": false, + "material_types": "[\"slides\"]", + "matman_roles": "[\"chair\",\"delegate\"]", + "req_subm_approval": true, + "role_order": "[\"chair\",\"secr\"]", + "show_on_agenda": false }, "model": "group.groupfeatures", "pk": "ietf" @@ -2447,8 +2492,11 @@ { "fields": { "about_page": "ietf.group.views.group_about", - "admin_roles": "chair", + "acts_like_wg": false, + "admin_roles": "[\"chair\"]", "agenda_type": null, + "create_wiki": false, + "custom_group_roles": true, "customize_workflow": false, "default_tab": "ietf.group.views.group_about", "has_chartering_process": false, @@ -2459,7 +2507,13 @@ "has_milestones": false, "has_nonsession_materials": false, "has_reviews": false, - "material_types": "slides" + "has_session_materials": false, + "is_schedulable": false, + "material_types": "[\"slides\"]", + "matman_roles": "[\"auth\"]", + "req_subm_approval": false, + "role_order": "[\"chair\",\"secr\"]", + "show_on_agenda": false }, "model": "group.groupfeatures", "pk": "individ" @@ -2467,8 +2521,11 @@ { "fields": { "about_page": "ietf.group.views.group_about", - "admin_roles": "chair", + "acts_like_wg": false, + "admin_roles": "[\"chair\"]", "agenda_type": "ietf", + "create_wiki": false, + "custom_group_roles": true, "customize_workflow": false, "default_tab": "ietf.group.views.group_about", "has_chartering_process": false, @@ -2479,7 +2536,13 @@ "has_milestones": false, "has_nonsession_materials": false, "has_reviews": false, - "material_types": "slides" + "has_session_materials": false, + "is_schedulable": false, + "material_types": "[\"slides\"]", + "matman_roles": "[\"chair\",\"delegate\",\"secr\"]", + "req_subm_approval": true, + "role_order": "[\"chair\",\"secr\"]", + "show_on_agenda": false }, "model": "group.groupfeatures", "pk": "irtf" @@ -2487,8 +2550,11 @@ { "fields": { "about_page": "ietf.group.views.group_about", - "admin_roles": "chair", + "acts_like_wg": false, + "admin_roles": "[\"chair\"]", "agenda_type": null, + "create_wiki": false, + "custom_group_roles": true, "customize_workflow": false, "default_tab": "ietf.group.views.group_about", "has_chartering_process": false, @@ -2499,7 +2565,13 @@ "has_milestones": false, "has_nonsession_materials": false, "has_reviews": false, - "material_types": "slides" + "has_session_materials": false, + "is_schedulable": false, + "material_types": "[\"slides\"]", + "matman_roles": "[\"chair\",\"secr\"]", + "req_subm_approval": true, + "role_order": "[\"chair\",\"secr\"]", + "show_on_agenda": false }, "model": "group.groupfeatures", "pk": "isoc" @@ -2507,8 +2579,11 @@ { "fields": { "about_page": "ietf.group.views.group_about", - "admin_roles": "chair", + "acts_like_wg": false, + "admin_roles": "[\"chair\"]", "agenda_type": "side", + "create_wiki": true, + "custom_group_roles": true, "customize_workflow": false, "default_tab": "ietf.group.views.group_about", "has_chartering_process": false, @@ -2519,7 +2594,13 @@ "has_milestones": false, "has_nonsession_materials": false, "has_reviews": false, - "material_types": "slides" + "has_session_materials": false, + "is_schedulable": false, + "material_types": "[\"slides\"]", + "matman_roles": "[\"chair\"]", + "req_subm_approval": true, + "role_order": "[\"chair\",\"member\",\"advisor\"]", + "show_on_agenda": false }, "model": "group.groupfeatures", "pk": "nomcom" @@ -2527,8 +2608,11 @@ { "fields": { "about_page": "ietf.group.views.group_about", - "admin_roles": "lead", + "acts_like_wg": false, + "admin_roles": "[\"lead\"]", "agenda_type": null, + "create_wiki": false, + "custom_group_roles": true, "customize_workflow": false, "default_tab": "ietf.group.views.group_about", "has_chartering_process": false, @@ -2539,7 +2623,13 @@ "has_milestones": true, "has_nonsession_materials": false, "has_reviews": false, - "material_types": "slides" + "has_session_materials": false, + "is_schedulable": false, + "material_types": "[\"slides\"]", + "matman_roles": "[\"chair\",\"secr\"]", + "req_subm_approval": false, + "role_order": "[\"chair\",\"secr\"]", + "show_on_agenda": false }, "model": "group.groupfeatures", "pk": "program" @@ -2547,8 +2637,11 @@ { "fields": { "about_page": "ietf.group.views.group_about", - "admin_roles": "chair,secr", + "acts_like_wg": false, + "admin_roles": "[\"chair\",\"secr\"]", "agenda_type": null, + "create_wiki": true, + "custom_group_roles": true, "customize_workflow": false, "default_tab": "ietf.group.views.review_requests", "has_chartering_process": false, @@ -2559,7 +2652,13 @@ "has_milestones": false, "has_nonsession_materials": false, "has_reviews": true, - "material_types": "slides" + "has_session_materials": false, + "is_schedulable": false, + "material_types": "[\"slides\"]", + "matman_roles": "[\"ad\",\"secr\"]", + "req_subm_approval": true, + "role_order": "[\"chair\",\"secr\"]", + "show_on_agenda": false }, "model": "group.groupfeatures", "pk": "review" @@ -2567,8 +2666,11 @@ { "fields": { "about_page": "ietf.group.views.group_about", - "admin_roles": "chair", + "acts_like_wg": false, + "admin_roles": "[\"chair\"]", "agenda_type": "side", + "create_wiki": false, + "custom_group_roles": true, "customize_workflow": false, "default_tab": "ietf.group.views.group_about", "has_chartering_process": false, @@ -2579,7 +2681,13 @@ "has_milestones": false, "has_nonsession_materials": false, "has_reviews": false, - "material_types": "slides" + "has_session_materials": false, + "is_schedulable": false, + "material_types": "[\"slides\"]", + "matman_roles": "[\"chair\",\"secr\"]", + "req_subm_approval": true, + "role_order": "[\"chair\",\"secr\"]", + "show_on_agenda": false }, "model": "group.groupfeatures", "pk": "rfcedtyp" @@ -2587,8 +2695,11 @@ { "fields": { "about_page": "ietf.group.views.group_about", - "admin_roles": "chair", + "acts_like_wg": true, + "admin_roles": "[\"chair\"]", "agenda_type": "ietf", + "create_wiki": true, + "custom_group_roles": false, "customize_workflow": true, "default_tab": "ietf.group.views.group_documents", "has_chartering_process": true, @@ -2599,7 +2710,13 @@ "has_milestones": true, "has_nonsession_materials": false, "has_reviews": false, - "material_types": "slides" + "has_session_materials": true, + "is_schedulable": true, + "material_types": "[\"slides\"]", + "matman_roles": "[\"chair\",\"secr\"]", + "req_subm_approval": true, + "role_order": "[\"chair\",\"secr\"]", + "show_on_agenda": true }, "model": "group.groupfeatures", "pk": "rg" @@ -2607,8 +2724,11 @@ { "fields": { "about_page": "ietf.group.views.group_about", - "admin_roles": "chair", + "acts_like_wg": false, + "admin_roles": "[\"chair\"]", "agenda_type": null, + "create_wiki": false, + "custom_group_roles": true, "customize_workflow": false, "default_tab": "ietf.group.views.group_about", "has_chartering_process": false, @@ -2619,7 +2739,13 @@ "has_milestones": false, "has_nonsession_materials": false, "has_reviews": false, - "material_types": "slides" + "has_session_materials": false, + "is_schedulable": false, + "material_types": "[\"slides\"]", + "matman_roles": "[\"liaiman\",\"matman\"]", + "req_subm_approval": true, + "role_order": "[\"liaiman\"]", + "show_on_agenda": false }, "model": "group.groupfeatures", "pk": "sdo" @@ -2627,8 +2753,11 @@ { "fields": { "about_page": "ietf.group.views.group_about", - "admin_roles": "chair", + "acts_like_wg": false, + "admin_roles": "[\"chair\"]", "agenda_type": "ietf", + "create_wiki": true, + "custom_group_roles": true, "customize_workflow": false, "default_tab": "ietf.group.views.group_about", "has_chartering_process": false, @@ -2639,7 +2768,13 @@ "has_milestones": false, "has_nonsession_materials": true, "has_reviews": false, - "material_types": "slides" + "has_session_materials": false, + "is_schedulable": false, + "material_types": "[\"slides\"]", + "matman_roles": "[\"chair\",\"matman\"]", + "req_subm_approval": false, + "role_order": "[\"chair\",\"member\",\"matman\"]", + "show_on_agenda": false }, "model": "group.groupfeatures", "pk": "team" @@ -2647,8 +2782,11 @@ { "fields": { "about_page": "ietf.group.views.group_about", - "admin_roles": "chair", + "acts_like_wg": true, + "admin_roles": "[\"chair\"]", "agenda_type": "ietf", + "create_wiki": true, + "custom_group_roles": false, "customize_workflow": true, "default_tab": "ietf.group.views.group_documents", "has_chartering_process": true, @@ -2659,7 +2797,13 @@ "has_milestones": true, "has_nonsession_materials": false, "has_reviews": false, - "material_types": "slides" + "has_session_materials": true, + "is_schedulable": true, + "material_types": "[\"slides\"]", + "matman_roles": "[\"ad\",\"chair\",\"delegate\",\"secr\"]", + "req_subm_approval": true, + "role_order": "[\"chair\",\"secr\",\"delegate\"]", + "show_on_agenda": true }, "model": "group.groupfeatures", "pk": "wg" @@ -3360,7 +3504,9 @@ }, { "fields": { - "cc": [], + "cc": [ + "liaison_admin" + ], "desc": "Recipients for a message that a pending liaison statement needs approval", "to": [ "liaison_approvers" @@ -4134,6 +4280,14 @@ "model": "mailtrigger.recipient", "pk": "ipr_updatedipr_holders" }, + { + "fields": { + "desc": "Alias for secretariat liaison administration", + "template": "" + }, + "model": "mailtrigger.recipient", + "pk": "liaison_admin" + }, { "fields": { "desc": "The set of people who can approve this liasion statemetns", @@ -10532,7 +10686,7 @@ "fields": { "command": "xym", "switch": "--version", - "time": "2018-12-04T00:08:13.259", + "time": "2019-01-21T00:08:23.930", "used": true, "version": "xym 0.4" }, @@ -10543,7 +10697,7 @@ "fields": { "command": "pyang", "switch": "--version", - "time": "2018-12-04T00:08:13.958", + "time": "2019-01-21T00:08:25.229", "used": true, "version": "pyang 1.7.5" }, @@ -10554,7 +10708,7 @@ "fields": { "command": "yanglint", "switch": "--version", - "time": "2018-12-04T00:08:14.121", + "time": "2019-01-21T00:08:25.465", "used": true, "version": "yanglint 0.14.80" }, @@ -10565,9 +10719,9 @@ "fields": { "command": "xml2rfc", "switch": "--version", - "time": "2018-12-04T00:08:15.775", + "time": "2019-01-21T00:08:26.897", "used": true, - "version": "xml2rfc 2.15.2" + "version": "xml2rfc 2.16.3" }, "model": "utils.versioninfo", "pk": 4 diff --git a/ietf/review/utils.py b/ietf/review/utils.py index 32a0391e8..85497ba49 100644 --- a/ietf/review/utils.py +++ b/ietf/review/utils.py @@ -712,7 +712,7 @@ def setup_reviewer_field(field, review_req): def get_default_filter_re(person): if type(person) != Person: person = Person.objects.get(id=person) - groups_to_avoid = [r.group for r in person.role_set.filter(name='chair',group__type__in=['wg','rg'])] + groups_to_avoid = [ r.group for r in person.role_set.all() if r.name in r.group.features.admin_roles and r.group.features.acts_like_wg ] if not groups_to_avoid: return '^draft-%s-.*$' % ( person.last_name().lower(), ) else: diff --git a/ietf/secr/groups/forms.py b/ietf/secr/groups/forms.py index af4dfd63e..be53cf82b 100644 --- a/ietf/secr/groups/forms.py +++ b/ietf/secr/groups/forms.py @@ -78,7 +78,7 @@ class GroupModelForm(forms.ModelForm): parent = self.cleaned_data['parent'] type = self.cleaned_data['type'] - if type.slug in ('ag','wg','rg') and not parent: + if type.features.acts_like_wg and not parent: raise forms.ValidationError("This field is required.") return parent @@ -130,8 +130,7 @@ class RoleForm(forms.Form): self.group = kwargs.pop('group') super(RoleForm, self).__init__(*args,**kwargs) # this form is re-used in roles app, use different roles in select - # TODO: should 'ag' be excluded here as well? - if self.group.type.slug not in ('wg','rg'): + if self.group.features.custom_group_roles: self.fields['name'].queryset = RoleName.objects.all() # check for id within parenthesis to ensure name was selected from the list diff --git a/ietf/settings.py b/ietf/settings.py index effc041a4..f276a4a58 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -552,7 +552,7 @@ MAX_WG_DELEGATES = 3 # These states aren't available in forms with drop-down choices for new # document state: GROUP_STATES_WITH_EXTRA_PROCESSING = ["sub-pub", "rfc-edit", ] - +GROUP_TYPES_LISTED_ACTIVE = ['wg', 'rg', 'ag', 'team', 'dir', 'review', 'area', 'program', ] DATE_FORMAT = "Y-m-d" DATETIME_FORMAT = "Y-m-d H:i T" @@ -901,7 +901,8 @@ TRAC_ISSUE_URL_PATTERN = "https://trac.ietf.org/trac/%s/report/1" TRAC_SVN_DIR_PATTERN = "/a/svn/group/%s" #TRAC_SVN_URL_PATTERN = "https://svn.ietf.org/svn/group/%s/" -TRAC_CREATE_GROUP_TYPES = ['wg', 'rg', 'area', 'team', 'dir', 'review', 'ag', 'nomcom', ] +# The group types setting was replaced by a group feature entry 10 Jan 2019 +#TRAC_CREATE_GROUP_TYPES = ['wg', 'rg', 'area', 'team', 'dir', 'review', 'ag', 'nomcom', ] TRAC_CREATE_GROUP_STATES = ['bof', 'active', ] TRAC_CREATE_GROUP_ACRONYMS = ['iesg', 'iaoc', 'ietf', ] TRAC_CREATE_ADHOC_WIKIS = [ diff --git a/ietf/submit/forms.py b/ietf/submit/forms.py index 998dd957d..8069613e5 100644 --- a/ietf/submit/forms.py +++ b/ietf/submit/forms.py @@ -466,7 +466,7 @@ class PreapprovalForm(forms.Form): if not components[-1]: raise forms.ValidationError("Name ends with a dash.") acronym = components[2] - if acronym not in self.groups.values_list('acronym', flat=True): + if acronym not in [ g.acronym for g in self.groups ]: raise forms.ValidationError("Group acronym not recognized as one you can approve drafts for.") if Preapproval.objects.filter(name=n): diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py index 0371312ff..0a7335b24 100644 --- a/ietf/submit/utils.py +++ b/ietf/submit/utils.py @@ -556,7 +556,7 @@ def preapprovals_for_user(user): if has_role(user, "Secretariat"): return res - acronyms = [g.acronym for g in Group.objects.filter(role__person__user=user, type__in=("wg", "rg"))] + acronyms = [g.acronym for g in Group.objects.filter(role__person__user=user, type__features__acts_like_wg=True)] res = res.filter(name__regex="draft-[^-]+-(%s)-.*" % "|".join(acronyms)) diff --git a/ietf/submit/views.py b/ietf/submit/views.py index 9fe164f47..57e09f4da 100644 --- a/ietf/submit/views.py +++ b/ietf/submit/views.py @@ -1,4 +1,4 @@ -# Copyright The IETF Trust 2007, All Rights Reserved +# Copyright The IETF Trust 2007-2019, All Rights Reserved import re import base64 @@ -17,7 +17,7 @@ import debug # pyflakes:ignore from ietf.doc.models import Document, DocAlias, AddedMessageEvent from ietf.doc.utils import prettify_std_name -from ietf.group.models import Group +from ietf.group.models import Group, Role from ietf.ietfauth.utils import has_role, role_required from ietf.mailtrigger.utils import gather_address_lists from ietf.message.models import Message, MessageAttachment @@ -124,7 +124,9 @@ def api_submit(request): submission.submitter = user.person.formatted_email() docevent_from_submission(request, submission, desc="Uploaded new revision") - requires_group_approval = (submission.rev == '00' and submission.group and submission.group.type_id in ("wg", "rg", "ietf", "irtf", "iab", "iana", "rfcedtyp") and not Preapproval.objects.filter(name=submission.name).exists()) + requires_group_approval = (submission.rev == '00' + and submission.group and submission.group.features.req_subm_approval + and not Preapproval.objects.filter(name=submission.name).exists()) requires_prev_authors_approval = Document.objects.filter(name=submission.name) sent_to, desc, docDesc = send_confirmation_emails(request, submission, requires_group_approval, requires_prev_authors_approval) @@ -212,7 +214,9 @@ def submission_status(request, submission_id, access_token=None): confirmation_list = addrs.to confirmation_list.extend(addrs.cc) - requires_group_approval = (submission.rev == '00' and submission.group and submission.group.type_id in ("wg", "rg", "ietf", "irtf", "iab", "iana", "rfcedtyp") and not Preapproval.objects.filter(name=submission.name).exists()) + requires_group_approval = (submission.rev == '00' + and submission.group and submission.group.features.req_subm_approval + and not Preapproval.objects.filter(name=submission.name).exists()) requires_prev_authors_approval = Document.objects.filter(name=submission.name) @@ -506,10 +510,11 @@ def approvals(request): @role_required("Secretariat", "Area Director", "WG Chair", "RG Chair") def add_preapproval(request): - groups = Group.objects.filter(type__in=("wg", "rg")).exclude(state__in=["conclude","bof-conc"]).order_by("acronym").distinct() + groups = Group.objects.filter(type__features__acts_like_wg=True).exclude(state__in=["conclude","bof-conc"]).order_by("acronym").distinct() if not has_role(request.user, "Secretariat"): - groups = groups.filter(role__person__user=request.user,role__name__in=['ad','chair','delegate','secr']) + groups = [ g for g in groups.filter(role__person__user=request.user) + if Role.objects.filter(group=g, person__user=request.user, name__slug__in=g.type.features.matman_roles).exists() ] if request.method == "POST": form = PreapprovalForm(request.POST) diff --git a/ietf/utils/management/commands/create_group_wikis.py b/ietf/utils/management/commands/create_group_wikis.py index 0b299e42b..3ab2d7abc 100644 --- a/ietf/utils/management/commands/create_group_wikis.py +++ b/ietf/utils/management/commands/create_group_wikis.py @@ -19,7 +19,7 @@ from django.template.loader import render_to_string import debug # pyflakes:ignore -from ietf.group.models import Group, GroupURL +from ietf.group.models import Group, GroupURL, GroupFeatures from ietf.utils.pipe import pipe logtag = __name__.split('.')[-1] @@ -216,7 +216,7 @@ class Command(BaseCommand): self.maybe_add_group_url(group, 'Issue tracker', settings.TRAC_ISSUE_URL_PATTERN % group.acronym) # Use custom assets (if any) from the master setup self.symlink_to_master_assets(group.trac_dir, env) - if group.type_id in ['wg', 'rg', 'ag', ]: + if group.features.acts_like_wg: self.add_wg_draft_states(group, env) self.add_custom_wiki_pages(group, env) self.add_default_wiki_pages(env) @@ -338,7 +338,8 @@ class Command(BaseCommand): if not os.path.exists(os.path.dirname(self.svn_dir_pattern)): raise CommandError('The SVN base direcory specified for the SVN directories (%s) does not exist.' % os.path.dirname(self.svn_dir_pattern)) - gfilter = Q(type__slug__in=settings.TRAC_CREATE_GROUP_TYPES, state__slug__in=settings.TRAC_CREATE_GROUP_STATES) + gtypes = [ f.type for f in GroupFeatures.objects.filter(create_wiki=True) ] + gfilter = Q(type__in=gtypes, state__slug__in=settings.TRAC_CREATE_GROUP_STATES) gfilter |= Q(acronym__in=settings.TRAC_CREATE_GROUP_ACRONYMS) groups = Group.objects.filter(gfilter).order_by('acronym') diff --git a/ietf/utils/test_runner.py b/ietf/utils/test_runner.py index 4892d26b2..1ca9e94be 100644 --- a/ietf/utils/test_runner.py +++ b/ietf/utils/test_runner.py @@ -1,4 +1,6 @@ -# Copyright The IETF Trust 2007, All Rights Reserved +# Copyright The IETF Trust 2007-2019, All Rights Reserved +# -*- coding: utf-8 -*- + # Portion Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). # All rights reserved. Contact: Pasi Eronen @@ -422,6 +424,8 @@ class CoverageTest(unittest.TestCase): if issubclass(cl, ModelOperation) or issubclass(cl, FieldOperation): ops.append(('schema', cl.__name__)) elif issubclass(cl, Operation): + if getattr(op, 'code', None) and getattr(op.code, 'interleavable', None) == True: + continue ops.append(('data', cl.__name__)) else: raise RuntimeError("Found unexpected operation type in migration: %s" % (op)) @@ -441,10 +445,15 @@ class CoverageTest(unittest.TestCase): unreleased.append((node, op, nm)) # gather the transitions in operation types. We'll allow 1 # transition, but not 2 or more. - mixed = [ unreleased[i] for i in range(1,len(unreleased)) if unreleased[i][1] != unreleased[i-1][1] ] + for s in range(len(unreleased)): + # ignore leading data migrations, they run with the production + # schema so can take any time they like + if unreleased[s][1] != 'data': + break + mixed = [ unreleased[i] for i in range(s+1,len(unreleased)) if unreleased[i][1] != unreleased[i-1][1] ] if len(mixed) > 1: raise self.failureException('Found interleaved schema and data operations in unreleased migrations;' - ' please see if they can be re-ordered with all schema migrations before the data migrations:\n' + ' please see if they can be re-ordered with all data migrations before the schema migrations:\n' +('\n'.join([' %-6s: %-12s, %s (%s)'% (op, node.key[0], node.key[1], nm) for (node, op, nm) in unreleased ]))) class IetfTestRunner(DiscoverRunner): diff --git a/requirements.txt b/requirements.txt index 751a1f1d0..dc8b4d66d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ coverage>=4.0.1,!=4.0.2 #cssselect>=0.6.1 # for PyQuery decorator>=4.0.4 defusedxml>=0.4.1 # for TastyPie when ussing xml; not a declared dependency -Django>=1.11,<1.12 +Django>=1.11,!=1.11.18,<1.12 # 1.11.18 has problems exporting BinaryField from django.db.models django-bcrypt>=0.9.2 # for the BCrypt password hasher option. Remove when all bcrypt upgraded to argon2 django-bootstrap3>=8.2.1,<9.0.0 django-cors-headers>=2.4.0