diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000000..70741e4dc6 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Ran Black on the entire code base 2025-04-14 +118d0de77071253933a2c9ea16cc4f6c05dbf427 diff --git a/ietf/__init__.py b/ietf/__init__.py index 26124c3c67..b8ea409ad1 100644 --- a/ietf/__init__.py +++ b/ietf/__init__.py @@ -2,35 +2,49 @@ # -*- coding: utf-8 -*- -from . import checks # pyflakes:ignore +from . import checks # pyflakes:ignore # Version must stay in single quotes for automatic CI replace # Don't add patch number here: -__version__ = '1.0.0-dev' +__version__ = "1.0.0-dev" # Release hash must stay in single quotes for automatic CI replace -__release_hash__ = '' +__release_hash__ = "" # Release branch must stay in single quotes for automatic CI replace -__release_branch__ = '' +__release_branch__ = "" # set this to ".p1", ".p2", etc. after patching -__patch__ = "" +__patch__ = "" -if __version__ == '1.0.0-dev' and __release_hash__ == '' and __release_branch__ == '': +if __version__ == "1.0.0-dev" and __release_hash__ == "" and __release_branch__ == "": import subprocess - branch = subprocess.run( - ["/usr/bin/git", "branch", "--show-current"], - capture_output=True, - ).stdout.decode().strip() - git_hash = subprocess.run( - ["/usr/bin/git", "rev-parse", "head"], - capture_output=True, - ).stdout.decode().strip() - rev = subprocess.run( - ["/usr/bin/git", "describe", "--tags", git_hash], - capture_output=True, - ).stdout.decode().strip().split('-', 1)[0] + + branch = ( + subprocess.run( + ["/usr/bin/git", "branch", "--show-current"], + capture_output=True, + ) + .stdout.decode() + .strip() + ) + git_hash = ( + subprocess.run( + ["/usr/bin/git", "rev-parse", "head"], + capture_output=True, + ) + .stdout.decode() + .strip() + ) + rev = ( + subprocess.run( + ["/usr/bin/git", "describe", "--tags", git_hash], + capture_output=True, + ) + .stdout.decode() + .strip() + .split("-", 1)[0] + ) __version__ = f"{rev}-dev" __release_branch__ = branch __release_hash__ = git_hash @@ -40,4 +54,4 @@ # Django starts so that shared_task will use this app. from .celeryapp import app as celery_app -__all__ = ('celery_app',) +__all__ = ("celery_app",) diff --git a/ietf/admin/apps.py b/ietf/admin/apps.py index 20b762cfec..c72ebd4344 100644 --- a/ietf/admin/apps.py +++ b/ietf/admin/apps.py @@ -3,4 +3,4 @@ class AdminConfig(admin_apps.AdminConfig): - default_site = "ietf.admin.sites.AdminSite" + default_site = "ietf.admin.sites.AdminSite" diff --git a/ietf/admin/sites.py b/ietf/admin/sites.py index 69cb62ae20..d21722611e 100644 --- a/ietf/admin/sites.py +++ b/ietf/admin/sites.py @@ -6,10 +6,12 @@ class AdminSite(_AdminSite): site_title = "Datatracker admin" - + @staticmethod def site_header(): if settings.SERVER_MODE == "production": return "Datatracker administration" else: - return mark_safe('Datatracker administration δ') + return mark_safe( + 'Datatracker administration δ' + ) diff --git a/ietf/api/__init__.py b/ietf/api/__init__.py index 9fadab8e6f..91b046e54b 100644 --- a/ietf/api/__init__.py +++ b/ietf/api/__init__.py @@ -12,7 +12,7 @@ from django.utils.module_loading import autodiscover_modules -import debug # pyflakes:ignore +import debug # pyflakes:ignore import tastypie.resources import tastypie.serializers @@ -25,17 +25,19 @@ OMITTED_APPS_APIS = ["ietf.status"] + def populate_api_list(): _module_dict = globals() for app_config in django_apps.get_app_configs(): - if '.' in app_config.name and app_config.name not in OMITTED_APPS_APIS: - _root, _name = app_config.name.split('.', 1) - if _root == 'ietf': - if not '.' in _name: + if "." in app_config.name and app_config.name not in OMITTED_APPS_APIS: + _root, _name = app_config.name.split(".", 1) + if _root == "ietf": + if not "." in _name: _api = Api(api_name=_name) _module_dict[_name] = _api _api_list.append((_name, _api)) + def autodiscover(): """ Auto-discover INSTALLED_APPS resources.py modules and fail silently when @@ -52,17 +54,25 @@ def generate_cache_key(self, *args, **kwargs): This is based off the current api_name/resource_name/args/kwargs. """ - #smooshed = ["%s=%s" % (key, value) for key, value in kwargs.items()] + # smooshed = ["%s=%s" % (key, value) for key, value in kwargs.items()] smooshed = urlencode(kwargs) # Use a list plus a ``.join()`` because it's faster than concatenation. - return "%s:%s:%s:%s" % (self._meta.api_name, self._meta.resource_name, ':'.join(args), smooshed) + return "%s:%s:%s:%s" % ( + self._meta.api_name, + self._meta.resource_name, + ":".join(args), + smooshed, + ) + +TIMEDELTA_REGEX = re.compile( + r"^(?P\d+d)?\s?(?P\d+h)?\s?(?P\d+m)?\s?(?P\d+s?)$" +) -TIMEDELTA_REGEX = re.compile(r'^(?P\d+d)?\s?(?P\d+h)?\s?(?P\d+m)?\s?(?P\d+s?)$') class TimedeltaField(ApiField): - dehydrated_type = 'timedelta' + dehydrated_type = "timedelta" help_text = "A timedelta field, with duration expressed in seconds. Ex: 132" def convert(self, value): @@ -74,33 +84,53 @@ def convert(self, value): if match: data = match.groupdict() - return datetime.timedelta(int(data['days']), int(data['hours']), int(data['minutes']), int(data['seconds'])) + return datetime.timedelta( + int(data["days"]), + int(data["hours"]), + int(data["minutes"]), + int(data["seconds"]), + ) else: - raise ApiFieldError("Timedelta provided to '%s' field doesn't appear to be a valid timedelta string: '%s'" % (self.instance_name, value)) + raise ApiFieldError( + "Timedelta provided to '%s' field doesn't appear to be a valid timedelta string: '%s'" + % (self.instance_name, value) + ) return value def hydrate(self, bundle): value = super(TimedeltaField, self).hydrate(bundle) - if value and not hasattr(value, 'seconds'): + if value and not hasattr(value, "seconds"): if isinstance(value, str): try: match = TIMEDELTA_REGEX.search(value) if match: data = match.groupdict() - value = datetime.timedelta(int(data['days']), int(data['hours']), int(data['minutes']), int(data['seconds'])) + value = datetime.timedelta( + int(data["days"]), + int(data["hours"]), + int(data["minutes"]), + int(data["seconds"]), + ) else: raise ValueError() except (ValueError, TypeError): - raise ApiFieldError("Timedelta provided to '%s' field doesn't appear to be a valid datetime string: '%s'" % (self.instance_name, value)) + raise ApiFieldError( + "Timedelta provided to '%s' field doesn't appear to be a valid datetime string: '%s'" + % (self.instance_name, value) + ) else: - raise ApiFieldError("Datetime provided to '%s' field must be a string: %s" % (self.instance_name, value)) + raise ApiFieldError( + "Datetime provided to '%s' field must be a string: %s" + % (self.instance_name, value) + ) return value + class ToOneField(tastypie.fields.ToOneField): "Subclass of tastypie.fields.ToOneField which adds caching in the dehydrate method." @@ -108,7 +138,7 @@ def dehydrate(self, bundle, for_list=True): foreign_obj = None previous_obj = None attrib = None - + if callable(self.attribute): previous_obj = bundle.obj foreign_obj = self.attribute(bundle) @@ -126,24 +156,41 @@ def dehydrate(self, bundle, for_list=True): if not foreign_obj: if not self.null: if callable(self.attribute): - raise ApiFieldError("The related resource for resource %s could not be found." % (previous_obj)) + raise ApiFieldError( + "The related resource for resource %s could not be found." + % (previous_obj) + ) else: - raise ApiFieldError("The model '%r' has an empty attribute '%s' and doesn't allow a null value." % (previous_obj, attrib)) + raise ApiFieldError( + "The model '%r' has an empty attribute '%s' and doesn't allow a null value." + % (previous_obj, attrib) + ) return None fk_resource = self.get_related_resource(foreign_obj) # Up to this point we've copied the code from tastypie 0.13.1. Now # we add caching. - cache_key = fk_resource.generate_cache_key('related', pk=foreign_obj.pk, for_list=for_list, ) + cache_key = fk_resource.generate_cache_key( + "related", + pk=foreign_obj.pk, + for_list=for_list, + ) dehydrated = fk_resource._meta.cache.get(cache_key) if dehydrated is None: fk_bundle = Bundle(obj=foreign_obj, request=bundle.request) - dehydrated = self.dehydrate_related(fk_bundle, fk_resource, for_list=for_list) + dehydrated = self.dehydrate_related( + fk_bundle, fk_resource, for_list=for_list + ) fk_resource._meta.cache.set(cache_key, dehydrated) return dehydrated class Serializer(tastypie.serializers.Serializer): def format_datetime(self, data): - return data.astimezone(datetime.timezone.utc).replace(tzinfo=None).isoformat(timespec="seconds") + "Z" + return ( + data.astimezone(datetime.timezone.utc) + .replace(tzinfo=None) + .isoformat(timespec="seconds") + + "Z" + ) diff --git a/ietf/api/__init__.pyi b/ietf/api/__init__.pyi index ededea90a7..14c52ab0a3 100644 --- a/ietf/api/__init__.pyi +++ b/ietf/api/__init__.pyi @@ -3,30 +3,30 @@ from typing import Any, List import tastypie -community = ... # type: Any -dbtemplate = ... # type: Any -doc = ... # type: Any -group = ... # type: Any -iesg = ... # type: Any -ipr = ... # type: Any -liaisons = ... # type: Any -mailinglists = ... # type: Any -mailtrigger = ... # type: Any -meeting = ... # type: Any -message = ... # type: Any -name = ... # type: Any -nomcom = ... # type: Any -person = ... # type: Any -redirects = ... # type: Any -review = ... # type: Any -stats = ... # type: Any -submit = ... # type: Any -utils = ... # type: Any +community = ... # type: Any +dbtemplate = ... # type: Any +doc = ... # type: Any +group = ... # type: Any +iesg = ... # type: Any +ipr = ... # type: Any +liaisons = ... # type: Any +mailinglists = ... # type: Any +mailtrigger = ... # type: Any +meeting = ... # type: Any +message = ... # type: Any +name = ... # type: Any +nomcom = ... # type: Any +person = ... # type: Any +redirects = ... # type: Any +review = ... # type: Any +stats = ... # type: Any +submit = ... # type: Any +utils = ... # type: Any -_api_list = ... # type: List +_api_list = ... # type: List class ModelResource(tastypie.resources.ModelResource): ... -class Serializer(): ... +class Serializer: ... class ToOneField(tastypie.fields.ToOneField): ... class TimedeltaField(tastypie.fields.ApiField): ... diff --git a/ietf/api/apps.py b/ietf/api/apps.py index 4549e0d7f2..3e90ddb708 100644 --- a/ietf/api/apps.py +++ b/ietf/api/apps.py @@ -7,13 +7,13 @@ class ApiConfig(AppConfig): def ready(self): """Hook to do init after the app registry is fully populated - + Importing models or accessing the app registry is ok here, but do not - interact with the database. See + interact with the database. See https://docs.djangoproject.com/en/4.2/ref/applications/#django.apps.AppConfig.ready """ # Populate our API list now that the app registry is set up populate_api_list() - # Import drf-spectacular extensions + # Import drf-spectacular extensions import ietf.api.schema # pyflakes: ignore diff --git a/ietf/api/authentication.py b/ietf/api/authentication.py index dfab0d72b8..f64d1813c3 100644 --- a/ietf/api/authentication.py +++ b/ietf/api/authentication.py @@ -9,7 +9,7 @@ class ApiKeyAuthentication(authentication.BaseAuthentication): def authenticate(self, request): """Extract the authentication token, if present - + This does not validate the token, it just arranges for it to be available in request.auth. It's up to a Permissions class to validate it for the appropriate endpoint. """ diff --git a/ietf/api/management/commands/makeresources.py b/ietf/api/management/commands/makeresources.py index 889b2cdfb5..5229b1891f 100644 --- a/ietf/api/management/commands/makeresources.py +++ b/ietf/api/management/commands/makeresources.py @@ -8,7 +8,7 @@ from importlib import import_module -import debug # pyflakes:ignore +import debug # pyflakes:ignore from django.core.management.base import AppCommand from django.db import models @@ -54,11 +54,13 @@ class Meta: api.{{app_label}}.register({{model.name}}Resource()) {% endfor %}{% endautoescape %}""" + def render(template, dictionary): template = Template(template, None, None) context = Context(dictionary) return template.render(context) + class Command(AppCommand): def handle_app_config(self, app, **options): @@ -72,7 +74,7 @@ def handle_app_config(self, app, **options): app_resources = {} if os.path.exists(resource_file_path): resources = import_module("%s.resources" % app.name) - for n,v in resources.__dict__.items(): + for n, v in resources.__dict__.items(): if issubclass(type(v), type(ModelResource)): app_resources[n] = v @@ -86,46 +88,51 @@ def handle_app_config(self, app, **options): if missing_resources: print("Updating resources.py for %s" % app.name) with io.open(resource_file_path, "a") as rfile: - info = dict( - app=app.name, - app_label=app.label, - date=timezone.now() - ) + info = dict(app=app.name, app_label=app.label, date=timezone.now()) new_models = {} for model, rclass_name in missing_resources: model_name = model.__name__ resource_name = model.__name__.lower() - imports = collections.defaultdict(lambda: collections.defaultdict(list)) + imports = collections.defaultdict( + lambda: collections.defaultdict(list) + ) print("Adding resource class for %s" % model_name) foreign_keys = [] plain_names = [] fk_names = [] m2m_names = [] pk_name = model._meta.pk.name - #debug.pprint('dir(model)') + # debug.pprint('dir(model)') for field in model._meta.fields: - if isinstance(field, (models.ForeignKey, models.OneToOneField)): - #debug.show('field.name') - #debug.pprint('dir(field.remote_field.to)') - #exit() - rel_app=field.remote_field.model._meta.app_label - rel_model_name=field.remote_field.model.__name__ + if isinstance( + field, (models.ForeignKey, models.OneToOneField) + ): + # debug.show('field.name') + # debug.pprint('dir(field.remote_field.to)') + # exit() + rel_app = field.remote_field.model._meta.app_label + rel_model_name = field.remote_field.model.__name__ if rel_model_name == model_name: # foreign key to self class -- quote # the rmodel_name - rmodel_name="'%s.resources.%sResource'" % (app.name, rel_model_name) + rmodel_name = "'%s.resources.%sResource'" % ( + app.name, + rel_model_name, + ) else: - rmodel_name=rel_model_name+"Resource" - foreign_keys.append(dict( - field=field, - name=field.name, - app=rel_app, - module=rel_app.split('.')[-1], - model=field.remote_field.model, - model_name=rel_model_name, - rmodel_name=rmodel_name, - resource_name=field.remote_field.model.__name__.lower(), - )) + rmodel_name = rel_model_name + "Resource" + foreign_keys.append( + dict( + field=field, + name=field.name, + app=rel_app, + module=rel_app.split(".")[-1], + model=field.remote_field.model, + model_name=rel_model_name, + rmodel_name=rmodel_name, + resource_name=field.remote_field.model.__name__.lower(), + ) + ) imports[rel_app]["module"] = rel_app imports[rel_app]["names"].append(rel_model_name) fk_names.append(field.name) @@ -133,44 +140,49 @@ def handle_app_config(self, app, **options): plain_names.append(field.name) m2m_keys = [] for field in model._meta.many_to_many: - #debug.show('field.name') - #debug.pprint('dir(field.remote_field.model)') - #exit() - rel_app=field.remote_field.model._meta.app_label - rel_model_name=field.remote_field.model.__name__ - if rel_model_name == model_name: - # foreign key to self class -- quote - # the rmodel_name - rmodel_name="'%s.resources.%sResource'" % (app.name, rel_model_name) - else: - rmodel_name=rel_model_name+"Resource" - m2m_keys.append(dict( + # debug.show('field.name') + # debug.pprint('dir(field.remote_field.model)') + # exit() + rel_app = field.remote_field.model._meta.app_label + rel_model_name = field.remote_field.model.__name__ + if rel_model_name == model_name: + # foreign key to self class -- quote + # the rmodel_name + rmodel_name = "'%s.resources.%sResource'" % ( + app.name, + rel_model_name, + ) + else: + rmodel_name = rel_model_name + "Resource" + m2m_keys.append( + dict( field=field, name=field.name, app=rel_app, - module=rel_app.split('.')[-1], + module=rel_app.split(".")[-1], model=field.remote_field.model, model_name=rel_model_name, rmodel_name=rmodel_name, resource_name=field.remote_field.model.__name__.lower(), - )) - imports[rel_app]["module"] = rel_app - imports[rel_app]["names"].append(rel_model_name) - m2m_names.append(field.name) + ) + ) + imports[rel_app]["module"] = rel_app + imports[rel_app]["names"].append(rel_model_name) + m2m_names.append(field.name) # some special import cases if "auth" in imports: - imports["auth"]["module"] = 'utils' + imports["auth"]["module"] = "utils" if "contenttypes" in imports: - imports["contenttypes"]["module"] = 'utils' + imports["contenttypes"]["module"] = "utils" for k in imports: imports[k]["names"] = set(imports[k]["names"]) new_models[model_name] = dict( - app=app.name.split('.')[-1], + app=app.name.split(".")[-1], model=model, fields=model._meta.fields, m2m_fields=model._meta.many_to_many, name=model_name, - imports=[ v for k,v in list(imports.items()) ], + imports=[v for k, v in list(imports.items())], foreign_keys=foreign_keys, m2m_keys=m2m_keys, resource_name=resource_name, @@ -178,7 +190,9 @@ def handle_app_config(self, app, **options): fk_names=fk_names, m2m_names=m2m_names, pk_name=pk_name, - rn_comment=False if resource_name.endswith('resource') else True, + rn_comment=( + False if resource_name.endswith("resource") else True + ), ) # Sort resources according to internal FK reference depth @@ -190,38 +204,53 @@ def handle_app_config(self, app, **options): internal_fk_count_limit = 0 while len(new_models) > 0: list_len = len(new_models) - #debug.show('len(new_models)') + # debug.show('len(new_models)') keys = list(new_models.keys()) for model_name in keys: internal_fk_count = 0 - for fk in new_models[model_name]["foreign_keys"]+new_models[model_name]["m2m_keys"]: - #debug.say("if statement comparison on:") - #debug.show('fk["model_name"]') - #debug.show('model_name') - #debug.say('if fk["model_name"] in new_models and not fk["model_name"] == model_name:') - if fk["model_name"] in new_models and not fk["model_name"] == model_name: - #print("Not a leaf model: %s: found fk to %s" % (model_name, fk["model"])) + for fk in ( + new_models[model_name]["foreign_keys"] + + new_models[model_name]["m2m_keys"] + ): + # debug.say("if statement comparison on:") + # debug.show('fk["model_name"]') + # debug.show('model_name') + # debug.say('if fk["model_name"] in new_models and not fk["model_name"] == model_name:') + if ( + fk["model_name"] in new_models + and not fk["model_name"] == model_name + ): + # print("Not a leaf model: %s: found fk to %s" % (model_name, fk["model"])) internal_fk_count += 1 if internal_fk_count <= internal_fk_count_limit: - #print("Ordered: "+model_name) + # print("Ordered: "+model_name) new_model_list.append(new_models[model_name]) del new_models[model_name] if list_len == len(new_models): - #debug.show('list_len, len(new_models)') - print("Circular FK dependencies -- cannot order resource classes") + # debug.show('list_len, len(new_models)') + print( + "Circular FK dependencies -- cannot order resource classes" + ) if internal_fk_count_limit < list_len: print("Attempting a partial ordering ...") internal_fk_count_limit += 1 else: - print("Failed also with partial ordering, writing resource classes without ordering") - new_model_list = [ v for k,v in list(new_models.items()) ] + print( + "Failed also with partial ordering, writing resource classes without ordering" + ) + new_model_list = [ + v for k, v in list(new_models.items()) + ] break if rfile.tell() == 0: print("Writing resource file head") rfile.write(render(resource_head_template, info)) else: - print("\nNOTE: Not writing resource file head.\nYou may have to update the import from %s.models" % app.name) + print( + "\nNOTE: Not writing resource file head.\nYou may have to update the import from %s.models" + % app.name + ) info.update(dict(models=new_model_list)) rfile.write(render(resource_class_template, info)) diff --git a/ietf/api/management/commands/showapikeys.py b/ietf/api/management/commands/showapikeys.py index 48d2bec974..49b4947318 100644 --- a/ietf/api/management/commands/showapikeys.py +++ b/ietf/api/management/commands/showapikeys.py @@ -3,19 +3,28 @@ from django.core.management.base import BaseCommand -import debug # pyflakes:ignore +import debug # pyflakes:ignore from ietf.person.models import PersonalApiKey + class Command(BaseCommand): - help = 'Show existing personal API keys' + help = "Show existing personal API keys" def add_arguments(self, parser): pass def handle(self, *args, **options): format = "%-36s %-24s %-64s %-16s" - self.stdout.write(format % ('Endpoint', 'Login', 'Key', 'Used')) + self.stdout.write(format % ("Endpoint", "Login", "Key", "Used")) for key in PersonalApiKey.objects.filter(): - self.stdout.write(format % (key.endpoint, key.person.user.username, key.hash(), key.latest.strftime('%Y-%m-%d %H:%M') if key.latest else '')) + self.stdout.write( + format + % ( + key.endpoint, + key.person.user.username, + key.hash(), + key.latest.strftime("%Y-%m-%d %H:%M") if key.latest else "", + ) + ) diff --git a/ietf/api/permissions.py b/ietf/api/permissions.py index 8f7fdd026f..20329edeb3 100644 --- a/ietf/api/permissions.py +++ b/ietf/api/permissions.py @@ -6,10 +6,11 @@ class HasApiKey(permissions.BasePermission): """Permissions class that validates a token using is_valid_token - + The view class must indicate the relevant endpoint by setting `api_key_endpoint`. Must be used with an Authentication class that puts a token in request.auth. """ + def has_permission(self, request, view): endpoint = getattr(view, "api_key_endpoint", None) auth_token = getattr(request, "auth", None) @@ -20,6 +21,7 @@ def has_permission(self, request, view): class IsOwnPerson(permissions.BasePermission): """Permission to access own Person object""" + def has_object_permission(self, request, view, obj): if not (request.user.is_authenticated and hasattr(request.user, "person")): return False @@ -28,12 +30,11 @@ def has_object_permission(self, request, view, obj): class BelongsToOwnPerson(permissions.BasePermission): """Permission to access objects associated with own Person - + Requires that the object have a "person" field that indicates ownership. """ + def has_object_permission(self, request, view, obj): if not (request.user.is_authenticated and hasattr(request.user, "person")): return False - return ( - hasattr(obj, "person") and obj.person == request.user.person - ) + return hasattr(obj, "person") and obj.person == request.user.person diff --git a/ietf/api/routers.py b/ietf/api/routers.py index 745ddaa811..d38db6b826 100644 --- a/ietf/api/routers.py +++ b/ietf/api/routers.py @@ -3,12 +3,16 @@ from django.core.exceptions import ImproperlyConfigured from rest_framework import routers + class PrefixedSimpleRouter(routers.SimpleRouter): """SimpleRouter that adds a dot-separated prefix to its basename""" + def __init__(self, name_prefix="", *args, **kwargs): self.name_prefix = name_prefix if len(self.name_prefix) == 0 or self.name_prefix[-1] == ".": - raise ImproperlyConfigured("Cannot use a name_prefix that is empty or ends with '.'") + raise ImproperlyConfigured( + "Cannot use a name_prefix that is empty or ends with '.'" + ) super().__init__(*args, **kwargs) def get_default_basename(self, viewset): diff --git a/ietf/api/schema.py b/ietf/api/schema.py index 7340149685..58aa87482e 100644 --- a/ietf/api/schema.py +++ b/ietf/api/schema.py @@ -8,6 +8,7 @@ class ApiKeyAuthenticationScheme(OpenApiAuthenticationExtension): Used by drf-spectacular when rendering the OpenAPI schema """ + target_class = "ietf.api.authentication.ApiKeyAuthentication" name = "apiKeyAuth" diff --git a/ietf/api/serializer.py b/ietf/api/serializer.py index d5bca430e0..870df596f6 100644 --- a/ietf/api/serializer.py +++ b/ietf/api/serializer.py @@ -18,35 +18,42 @@ from django_stubs_ext import QuerySetAny -import debug # pyflakes:ignore +import debug # pyflakes:ignore def filter_from_queryargs(request): - #@debug.trace + # @debug.trace def fix_ranges(d): - for k,v in d.items(): + for k, v in d.items(): if v.startswith("[") and v.endswith("]"): - d[k] = [ s for s in v[1:-1].split(",") if s ] + d[k] = [s for s in v[1:-1].split(",") if s] elif "," in v: - d[k] = [ s for s in v.split(",") if s ] - if k.endswith('__in') and not isinstance(d[k], list): - d[k] = [ d[k] ] + d[k] = [s for s in v.split(",") if s] + if k.endswith("__in") and not isinstance(d[k], list): + d[k] = [d[k]] return d + def is_ascii(s): return all(ord(c) < 128 for c in s) + # limit parameter keys to ascii. - params = dict( (k,v) for (k,v) in list(request.GET.items()) if is_ascii(k) ) - filter = fix_ranges(dict([(k,params[k]) for k in list(params.keys()) if not k.startswith("not__")])) - exclude = fix_ranges(dict([(k[5:],params[k]) for k in list(params.keys()) if k.startswith("not__")])) + params = dict((k, v) for (k, v) in list(request.GET.items()) if is_ascii(k)) + filter = fix_ranges( + dict([(k, params[k]) for k in list(params.keys()) if not k.startswith("not__")]) + ) + exclude = fix_ranges( + dict([(k[5:], params[k]) for k in list(params.keys()) if k.startswith("not__")]) + ) return filter, exclude + def unique_obj_name(obj): - """Return a unique string representation for an object, based on app, class and ID - """ + """Return a unique string representation for an object, based on app, class and ID""" app = obj._meta.app_label model = obj.__class__.__name__.lower() id = obj.pk - return "%s.%s[%s]" % (app,model,id) + return "%s.%s[%s]" % (app, model, id) + def cached_get(key, calculate_value, timeout=None): """Try to get value from cache using key. If no value exists calculate @@ -57,23 +64,32 @@ def cached_get(key, calculate_value, timeout=None): cache.set(key, value, timeout) return value + def model_top_level_cache_key(model): - return model.__module__ + '.' + model._meta.model.__name__ + return model.__module__ + "." + model._meta.model.__name__ + def clear_top_level_cache(sender, instance, *args, **kwargs): cache.delete(model_top_level_cache_key(instance)) -def clear_top_level_cache_m2m(sender, instance, action, reverse, model, *args, **kwargs): + +def clear_top_level_cache_m2m( + sender, instance, action, reverse, model, *args, **kwargs +): # Purge cache for both models affected and the potentially custom 'through' model - cache.delete_many(( - model_top_level_cache_key(instance), - model_top_level_cache_key(model), - model_top_level_cache_key(sender), - )) + cache.delete_many( + ( + model_top_level_cache_key(instance), + model_top_level_cache_key(model), + model_top_level_cache_key(sender), + ) + ) + + +post_save.connect(clear_top_level_cache, dispatch_uid="clear_top_level_cache") +post_delete.connect(clear_top_level_cache, dispatch_uid="clear_top_level_cache") +m2m_changed.connect(clear_top_level_cache_m2m, dispatch_uid="clear_top_level_cache") -post_save.connect(clear_top_level_cache, dispatch_uid='clear_top_level_cache') -post_delete.connect(clear_top_level_cache, dispatch_uid='clear_top_level_cache') -m2m_changed.connect(clear_top_level_cache_m2m, dispatch_uid='clear_top_level_cache') class AdminJsonSerializer(Serializer): """ @@ -97,17 +113,22 @@ class AdminJsonSerializer(Serializer): use_natural_keys = False def serialize(self, queryset, **options): - qi = options.get('query_info', '').encode('utf-8') + qi = options.get("query_info", "").encode("utf-8") if len(list(queryset)) == 1: obj = queryset[0] - key = 'json:%s:%s' % (hashlib.md5(qi).hexdigest(), unique_obj_name(obj)) + key = "json:%s:%s" % (hashlib.md5(qi).hexdigest(), unique_obj_name(obj)) is_cached = cache.get(model_top_level_cache_key(obj)) is True if is_cached: - value = cached_get(key, lambda: super(AdminJsonSerializer, self).serialize(queryset, **options)) + value = cached_get( + key, + lambda: super(AdminJsonSerializer, self).serialize( + queryset, **options + ), + ) else: - value = super(AdminJsonSerializer, self).serialize(queryset, **options) - cache.set(key, value) - cache.set(model_top_level_cache_key(obj), True) + value = super(AdminJsonSerializer, self).serialize(queryset, **options) + cache.set(key, value) + cache.set(model_top_level_cache_key(obj), True) return value else: return super(AdminJsonSerializer, self).serialize(queryset, **options) @@ -121,14 +142,18 @@ def get_dump_object(self, obj): return self._current def end_object(self, obj): - expansions = [ n.split("__")[0] for n in self.options.get('expand', []) if n ] + expansions = [n.split("__")[0] for n in self.options.get("expand", []) if n] for name in expansions: try: field = getattr(obj, name) - #self._current["_"+name] = smart_str(field) + # self._current["_"+name] = smart_str(field) if not isinstance(field, Field): options = self.options.copy() - options["expand"] = [ v[len(name)+2:] for v in options["expand"] if v.startswith(name+"__") ] + options["expand"] = [ + v[len(name) + 2 :] + for v in options["expand"] + if v.startswith(name + "__") + ] if hasattr(field, "all"): if options["expand"]: # If the following code (doing qs.select_related() is commented out it @@ -137,10 +162,20 @@ def end_object(self, obj): # models pulled in by select_related(). If that's acceptable, we can # comment this in again later. (The problem is known, captured in # Django issue #15040: https://code.djangoproject.com/ticket/15040 - self._current[name] = dict([ (rel.pk, self.expand_related(rel, name)) for rel in field.all().select_related() ]) + self._current[name] = dict( + [ + (rel.pk, self.expand_related(rel, name)) + for rel in field.all().select_related() + ] + ) # self._current[name] = dict([ (rel.pk, self.expand_related(rel, name)) for rel in field.all() ]) else: - self._current[name] = dict([ (rel.pk, self.expand_related(rel, name)) for rel in field.all() ]) + self._current[name] = dict( + [ + (rel.pk, self.expand_related(rel, name)) + for rel in field.all() + ] + ) else: if callable(field): try: @@ -149,35 +184,58 @@ def end_object(self, obj): field_value = None else: field_value = field - if isinstance(field_value, QuerySetAny) or isinstance(field_value, list): - self._current[name] = dict([ (rel.pk, self.expand_related(rel, name)) for rel in field_value ]) + if isinstance(field_value, QuerySetAny) or isinstance( + field_value, list + ): + self._current[name] = dict( + [ + (rel.pk, self.expand_related(rel, name)) + for rel in field_value + ] + ) else: if hasattr(field_value, "_meta"): - self._current[name] = self.expand_related(field_value, name) + self._current[name] = self.expand_related( + field_value, name + ) else: self._current[name] = str(field_value) except ObjectDoesNotExist: pass except AttributeError: names = [f.name for f in obj._meta.get_fields()] - if name in names and hasattr(obj, '%s_set' % name): - related_objects = getattr(obj, '%s_set' % name).all() + if name in names and hasattr(obj, "%s_set" % name): + related_objects = getattr(obj, "%s_set" % name).all() if self.options["expand"]: - self._current[name] = dict([(rel.pk, self.expand_related(rel, name)) for rel in related_objects.select_related()]) + self._current[name] = dict( + [ + (rel.pk, self.expand_related(rel, name)) + for rel in related_objects.select_related() + ] + ) else: - self._current[name] = dict([(rel.pk, self.expand_related(rel, name)) for rel in related_objects]) + self._current[name] = dict( + [ + (rel.pk, self.expand_related(rel, name)) + for rel in related_objects + ] + ) else: - raise FieldError("Cannot resolve keyword '%s' into field. " - "Choices are: %s" % (name, ", ".join(names))) + raise FieldError( + "Cannot resolve keyword '%s' into field. " + "Choices are: %s" % (name, ", ".join(names)) + ) super(AdminJsonSerializer, self).end_object(obj) def expand_related(self, related, name): options = self.options.copy() - options["expand"] = [ v[len(name)+2:] for v in options["expand"] if v.startswith(name+"__") ] - bytes = self.__class__().serialize([ related ], **options) + options["expand"] = [ + v[len(name) + 2 :] for v in options["expand"] if v.startswith(name + "__") + ] + bytes = self.__class__().serialize([related], **options) data = json.loads(bytes)[0] - if 'password' in data: - del data['password'] + if "password" in data: + del data["password"] return data def handle_fk_field(self, obj, field): @@ -186,49 +244,57 @@ def handle_fk_field(self, obj, field): except ObjectDoesNotExist: related = None if related is not None: - if field.name in self.options.get('expand', []): + if field.name in self.options.get("expand", []): related = self.expand_related(related, field.name) - elif self.use_natural_keys and hasattr(related, 'natural_key'): + elif self.use_natural_keys and hasattr(related, "natural_key"): related = related.natural_key() elif field.remote_field.field_name == related._meta.pk.name: # Related to remote object via primary key related = smart_str(related._get_pk_val(), strings_only=True) else: # Related to remote object via other field - related = smart_str(getattr(related, field.remote_field.field_name), strings_only=True) + related = smart_str( + getattr(related, field.remote_field.field_name), strings_only=True + ) self._current[field.name] = related def handle_m2m_field(self, obj, field): if field.remote_field.through._meta.auto_created: - if field.name in self.options.get('expand', []): + if field.name in self.options.get("expand", []): m2m_value = lambda value: self.expand_related(value, field.name) - elif self.use_natural_keys and hasattr(field.remote_field.to, 'natural_key'): + elif self.use_natural_keys and hasattr( + field.remote_field.to, "natural_key" + ): m2m_value = lambda value: value.natural_key() else: - m2m_value = lambda value: smart_str(value._get_pk_val(), strings_only=True) - self._current[field.name] = [m2m_value(related) - for related in getattr(obj, field.name).iterator()] + m2m_value = lambda value: smart_str( + value._get_pk_val(), strings_only=True + ) + self._current[field.name] = [ + m2m_value(related) for related in getattr(obj, field.name).iterator() + ] + class JsonExportMixin(object): """ Adds JSON export to a DetailView """ -# def json_object(self, request, object_id, extra_context=None): -# "The json view for an object of this model." -# try: -# obj = self.get_queryset().get(pk=unquote(object_id)) -# except self.model.DoesNotExist: -# # Don't raise Http404 just yet, because we haven't checked -# # permissions yet. We don't want an unauthenticated user to be able -# # to determine whether a given object exists. -# obj = None -# -# if obj is None: -# raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_str(self.model._meta.verbose_name), 'key': escape(object_id)}) -# -# content_type = 'application/json' -# return HttpResponse(serialize([ obj ], sort_keys=True, indent=3)[2:-2], content_type=content_type) + # def json_object(self, request, object_id, extra_context=None): + # "The json view for an object of this model." + # try: + # obj = self.get_queryset().get(pk=unquote(object_id)) + # except self.model.DoesNotExist: + # # Don't raise Http404 just yet, because we haven't checked + # # permissions yet. We don't want an unauthenticated user to be able + # # to determine whether a given object exists. + # obj = None + # + # if obj is None: + # raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_str(self.model._meta.verbose_name), 'key': escape(object_id)}) + # + # content_type = 'application/json' + # return HttpResponse(serialize([ obj ], sort_keys=True, indent=3)[2:-2], content_type=content_type) def json_view(self, request, filter=None, expand=None): if expand is None: @@ -240,12 +306,12 @@ def json_view(self, request, filter=None, expand=None): if k.startswith("_"): del qfilter[k] # discard a possible apikey, rather than using it as a queryset argument - if 'apikey' in qfilter: - del qfilter['apikey'] + if "apikey" in qfilter: + del qfilter["apikey"] qfilter.update(filter) filter = qfilter key = request.GET.get("_key", "pk") - exp = [ e for e in request.GET.get("_expand", "").split(",") if e ] + exp = [e for e in request.GET.get("_expand", "").split(",") if e] for e in exp: while True: expand.append(e) @@ -254,20 +320,31 @@ def json_view(self, request, filter=None, expand=None): e = e.rsplit("__", 1)[0] # expand = set(expand) - content_type = 'application/json' + content_type = "application/json" query_info = "%s?%s" % (request.META["PATH_INFO"], request.META["QUERY_STRING"]) try: qs = self.get_queryset().filter(**filter).exclude(**exclude) except (FieldError, ValueError) as e: - return HttpResponse(json.dumps({"error": str(e)}, sort_keys=True, indent=3), content_type=content_type) + return HttpResponse( + json.dumps({"error": str(e)}, sort_keys=True, indent=3), + content_type=content_type, + ) try: if expand: qs = qs.select_related() serializer = AdminJsonSerializer() - items = [(getattr(o, key), serializer.serialize([o], expand=expand, query_info=query_info) ) for o in qs ] - qd = dict( ( k, json.loads(v)[0] ) for k,v in items ) + items = [ + ( + getattr(o, key), + serializer.serialize([o], expand=expand, query_info=query_info), + ) + for o in qs + ] + qd = dict((k, json.loads(v)[0]) for k, v in items) except (FieldError, ValueError) as e: - return HttpResponse(json.dumps({"error": str(e)}, sort_keys=True, indent=3), content_type=content_type) + return HttpResponse( + json.dumps({"error": str(e)}, sort_keys=True, indent=3), + content_type=content_type, + ) text = json.dumps({smart_str(self.model._meta): qd}, sort_keys=True, indent=3) return HttpResponse(text, content_type=content_type) - diff --git a/ietf/api/tests.py b/ietf/api/tests.py index 7cc56c2d0a..d3868bc56a 100644 --- a/ietf/api/tests.py +++ b/ietf/api/tests.py @@ -22,7 +22,7 @@ from tastypie.test import ResourceTestCaseMixin -import debug # pyflakes:ignore +import debug # pyflakes:ignore import ietf from ietf.doc.storage_utils import retrieve_str @@ -34,108 +34,212 @@ from ietf.meeting.models import Session from ietf.nomcom.models import Volunteer from ietf.nomcom.factories import NomComFactory, nomcom_kwargs_for_year -from ietf.person.factories import PersonFactory, random_faker, EmailFactory, PersonalApiKeyFactory +from ietf.person.factories import ( + PersonFactory, + random_faker, + EmailFactory, + PersonalApiKeyFactory, +) from ietf.person.models import Email, User from ietf.stats.models import MeetingRegistration from ietf.utils.mail import empty_outbox, outbox, get_payload_text from ietf.utils.models import DumpInfo -from ietf.utils.test_utils import TestCase, login_testing_unauthorized, reload_db_objects +from ietf.utils.test_utils import ( + TestCase, + login_testing_unauthorized, + reload_db_objects, +) from .ietf_utils import is_valid_token, requires_api_token from .views import EmailIngestionError OMITTED_APPS = ( - 'ietf.secr.meetings', - 'ietf.secr.proceedings', - 'ietf.ipr', - 'ietf.status', + "ietf.secr.meetings", + "ietf.secr.proceedings", + "ietf.ipr", + "ietf.status", ) + class CustomApiTests(TestCase): - settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ['AGENDA_PATH'] + settings_temp_path_overrides = TestCase.settings_temp_path_overrides + [ + "AGENDA_PATH" + ] def test_api_help_page(self): - url = urlreverse('ietf.api.views.api_help') + url = urlreverse("ietf.api.views.api_help") r = self.client.get(url) - self.assertContains(r, 'The datatracker API', status_code=200) + self.assertContains(r, "The datatracker API", status_code=200) def test_api_openid_issuer(self): - url = urlreverse('ietf.api.urls.oidc_issuer') + url = urlreverse("ietf.api.urls.oidc_issuer") r = self.client.get(url) - self.assertContains(r, 'OpenID Connect Issuer', status_code=200) + self.assertContains(r, "OpenID Connect Issuer", status_code=200) def test_deprecated_api_set_session_video_url(self): - url = urlreverse('ietf.meeting.views.api_set_session_video_url') - recmanrole = RoleFactory(group__type_id='ietf', name_id='recman') + url = urlreverse("ietf.meeting.views.api_set_session_video_url") + recmanrole = RoleFactory(group__type_id="ietf", name_id="recman") recman = recmanrole.person - meeting = MeetingFactory(type_id='ietf') - session = SessionFactory(group__type_id='wg', meeting=meeting) + meeting = MeetingFactory(type_id="ietf") + session = SessionFactory(group__type_id="wg", meeting=meeting) group = session.group apikey = PersonalApiKeyFactory(endpoint=url, person=recman) - video = 'https://foo.example.com/bar/beer/' + video = "https://foo.example.com/bar/beer/" # error cases r = self.client.post(url, {}) self.assertContains(r, "Missing apikey parameter", status_code=400) - badrole = RoleFactory(group__type_id='ietf', name_id='ad') + badrole = RoleFactory(group__type_id="ietf", name_id="ad") badapikey = PersonalApiKeyFactory(endpoint=url, person=badrole.person) badrole.person.user.last_login = timezone.now() badrole.person.user.save() - r = self.client.post(url, {'apikey': badapikey.hash()} ) + r = self.client.post(url, {"apikey": badapikey.hash()}) self.assertContains(r, "Restricted to role: Recording Manager", status_code=403) - r = self.client.post(url, {'apikey': apikey.hash()} ) + r = self.client.post(url, {"apikey": apikey.hash()}) self.assertContains(r, "Too long since last regular login", status_code=400) recman.user.last_login = timezone.now() recman.user.save() - r = self.client.get(url, {'apikey': apikey.hash()} ) + r = self.client.get(url, {"apikey": apikey.hash()}) self.assertContains(r, "Method not allowed", status_code=405) - r = self.client.post(url, {'apikey': apikey.hash(), 'group': group.acronym} ) + r = self.client.post(url, {"apikey": apikey.hash(), "group": group.acronym}) self.assertContains(r, "Missing meeting parameter", status_code=400) - - r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, } ) + r = self.client.post( + url, + { + "apikey": apikey.hash(), + "meeting": meeting.number, + }, + ) self.assertContains(r, "Missing group parameter", status_code=400) - r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym} ) + r = self.client.post( + url, + { + "apikey": apikey.hash(), + "meeting": meeting.number, + "group": group.acronym, + }, + ) self.assertContains(r, "Missing item parameter", status_code=400) - r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym, 'item': '1'} ) + r = self.client.post( + url, + { + "apikey": apikey.hash(), + "meeting": meeting.number, + "group": group.acronym, + "item": "1", + }, + ) self.assertContains(r, "Missing url parameter", status_code=400) - r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': '1', 'group': group.acronym, - 'item': '1', 'url': video, }) + r = self.client.post( + url, + { + "apikey": apikey.hash(), + "meeting": "1", + "group": group.acronym, + "item": "1", + "url": video, + }, + ) self.assertContains(r, "No sessions found for meeting", status_code=400) - r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': 'bogous', - 'item': '1', 'url': video, }) - self.assertContains(r, "No sessions found in meeting '%s' for group 'bogous'"%meeting.number, status_code=400) + r = self.client.post( + url, + { + "apikey": apikey.hash(), + "meeting": meeting.number, + "group": "bogous", + "item": "1", + "url": video, + }, + ) + self.assertContains( + r, + "No sessions found in meeting '%s' for group 'bogous'" % meeting.number, + status_code=400, + ) - r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym, - 'item': '1', 'url': "foobar", }) + r = self.client.post( + url, + { + "apikey": apikey.hash(), + "meeting": meeting.number, + "group": group.acronym, + "item": "1", + "url": "foobar", + }, + ) self.assertContains(r, "Invalid url value: 'foobar'", status_code=400) - r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym, - 'item': '5', 'url': video, }) - self.assertContains(r, "No item '5' found in list of sessions for group", status_code=400) + r = self.client.post( + url, + { + "apikey": apikey.hash(), + "meeting": meeting.number, + "group": group.acronym, + "item": "5", + "url": video, + }, + ) + self.assertContains( + r, "No item '5' found in list of sessions for group", status_code=400 + ) - r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym, - 'item': 'foo', 'url': video, }) - self.assertContains(r, "Expected a numeric value for 'item', found 'foo'", status_code=400) + r = self.client.post( + url, + { + "apikey": apikey.hash(), + "meeting": meeting.number, + "group": group.acronym, + "item": "foo", + "url": video, + }, + ) + self.assertContains( + r, "Expected a numeric value for 'item', found 'foo'", status_code=400 + ) - r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym, - 'item': '1', 'url': video+'/rum', }) + r = self.client.post( + url, + { + "apikey": apikey.hash(), + "meeting": meeting.number, + "group": group.acronym, + "item": "1", + "url": video + "/rum", + }, + ) self.assertContains(r, "Done", status_code=200) - r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym, - 'item': '1', 'url': video+'/rum', }) + r = self.client.post( + url, + { + "apikey": apikey.hash(), + "meeting": meeting.number, + "group": group.acronym, + "item": "1", + "url": video + "/rum", + }, + ) self.assertContains(r, "URL is the same", status_code=400) - r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym, - 'item': '1', 'url': video, }) + r = self.client.post( + url, + { + "apikey": apikey.hash(), + "meeting": meeting.number, + "group": group.acronym, + "item": "1", + "url": video, + }, + ) self.assertContains(r, "Done", status_code=200) recordings = session.recordings() @@ -285,19 +389,18 @@ def test_api_set_meetecho_recording_name(self): session.refresh_from_db() self.assertEqual(session.meetecho_recording_name, name) - def test_api_add_session_attendees_deprecated(self): # Deprecated test - should be removed when we stop accepting a simple list of user PKs in # the add_session_attendees() view - url = urlreverse('ietf.meeting.views.api_add_session_attendees') + url = urlreverse("ietf.meeting.views.api_add_session_attendees") otherperson = PersonFactory() - recmanrole = RoleFactory(group__type_id='ietf', name_id='recman') + recmanrole = RoleFactory(group__type_id="ietf", name_id="recman") recman = recmanrole.person - meeting = MeetingFactory(type_id='ietf') - session = SessionFactory(group__type_id='wg', meeting=meeting) + meeting = MeetingFactory(type_id="ietf") + session = SessionFactory(group__type_id="wg", meeting=meeting) apikey = PersonalApiKeyFactory(endpoint=url, person=recman) - badrole = RoleFactory(group__type_id='ietf', name_id='ad') + badrole = RoleFactory(group__type_id="ietf", name_id="ad") badapikey = PersonalApiKeyFactory(endpoint=url, person=badrole.person) badrole.person.user.last_login = timezone.now() badrole.person.user.save() @@ -306,51 +409,69 @@ def test_api_add_session_attendees_deprecated(self): r = self.client.post(url, {}) self.assertContains(r, "Missing apikey parameter", status_code=400) - r = self.client.post(url, {'apikey': badapikey.hash()} ) + r = self.client.post(url, {"apikey": badapikey.hash()}) self.assertContains(r, "Restricted to role: Recording Manager", status_code=403) - r = self.client.post(url, {'apikey': apikey.hash()} ) + r = self.client.post(url, {"apikey": apikey.hash()}) self.assertContains(r, "Too long since last regular login", status_code=400) - recman.user.last_login = timezone.now()-datetime.timedelta(days=365) - recman.user.save() - r = self.client.post(url, {'apikey': apikey.hash()} ) + recman.user.last_login = timezone.now() - datetime.timedelta(days=365) + recman.user.save() + r = self.client.post(url, {"apikey": apikey.hash()}) self.assertContains(r, "Too long since last regular login", status_code=400) recman.user.last_login = timezone.now() recman.user.save() - r = self.client.get(url, {'apikey': apikey.hash()} ) + r = self.client.get(url, {"apikey": apikey.hash()}) self.assertContains(r, "Method not allowed", status_code=405) recman.user.last_login = timezone.now() recman.user.save() # Malformed requests - r = self.client.post(url, {'apikey': apikey.hash()} ) + r = self.client.post(url, {"apikey": apikey.hash()}) self.assertContains(r, "Missing attended parameter", status_code=400) for baddict in ( - '{}', + "{}", '{"bogons;drop table":"bogons;drop table"}', '{"session_id":"Not an integer;drop table"}', f'{{"session_id":{session.pk},"attendees":"not a list;drop table"}}', f'{{"session_id":{session.pk},"attendees":"not a list;drop table"}}', f'{{"session_id":{session.pk},"attendees":[1,2,"not an int;drop table",4]}}', ): - r = self.client.post(url, {'apikey': apikey.hash(), 'attended': baddict}) + r = self.client.post(url, {"apikey": apikey.hash(), "attended": baddict}) self.assertContains(r, "Malformed post", status_code=400) - bad_session_id = Session.objects.order_by('-pk').first().pk + 1 - r = self.client.post(url, {'apikey': apikey.hash(), 'attended': f'{{"session_id":{bad_session_id},"attendees":[]}}'}) + bad_session_id = Session.objects.order_by("-pk").first().pk + 1 + r = self.client.post( + url, + { + "apikey": apikey.hash(), + "attended": f'{{"session_id":{bad_session_id},"attendees":[]}}', + }, + ) self.assertContains(r, "Invalid session", status_code=400) - bad_user_id = User.objects.order_by('-pk').first().pk + 1 - r = self.client.post(url, {'apikey': apikey.hash(), 'attended': f'{{"session_id":{session.pk},"attendees":[{bad_user_id}]}}'}) + bad_user_id = User.objects.order_by("-pk").first().pk + 1 + r = self.client.post( + url, + { + "apikey": apikey.hash(), + "attended": f'{{"session_id":{session.pk},"attendees":[{bad_user_id}]}}', + }, + ) self.assertContains(r, "Invalid attendee", status_code=400) # Reasonable request - r = self.client.post(url, {'apikey':apikey.hash(), 'attended': f'{{"session_id":{session.pk},"attendees":[{recman.user.pk}, {otherperson.user.pk}]}}'}) + r = self.client.post( + url, + { + "apikey": apikey.hash(), + "attended": f'{{"session_id":{session.pk},"attendees":[{recman.user.pk}, {otherperson.user.pk}]}}', + }, + ) - self.assertEqual(session.attended_set.count(),2) + self.assertEqual(session.attended_set.count(), 2) self.assertTrue(session.attended_set.filter(person=recman).exists()) self.assertTrue(session.attended_set.filter(person=otherperson).exists()) @@ -469,16 +590,16 @@ def test_api_add_session_attendees(self): ) def test_api_upload_polls_and_chatlog(self): - recmanrole = RoleFactory(group__type_id='ietf', name_id='recman') + recmanrole = RoleFactory(group__type_id="ietf", name_id="recman") recmanrole.person.user.last_login = timezone.now() recmanrole.person.user.save() - badrole = RoleFactory(group__type_id='ietf', name_id='ad') + badrole = RoleFactory(group__type_id="ietf", name_id="ad") badrole.person.user.last_login = timezone.now() badrole.person.user.save() - meeting = MeetingFactory(type_id='ietf') - session = SessionFactory(group__type_id='wg', meeting=meeting) + meeting = MeetingFactory(type_id="ietf") + session = SessionFactory(group__type_id="wg", meeting=meeting) for type_id, content in ( ( @@ -494,7 +615,7 @@ def test_api_upload_polls_and_chatlog(self): "text": "

But software is not a thing.

", "time": "2022-07-28T19:26:45Z" } - ]""" + ]""", ), ( "polls", @@ -513,7 +634,7 @@ def test_api_upload_polls_and_chatlog(self): "raise_hand": 31, "do_not_raise_hand": 31 } - ]""" + ]""", ), ): url = urlreverse(f"ietf.meeting.views.api_upload_{type_id}") @@ -523,40 +644,59 @@ def test_api_upload_polls_and_chatlog(self): r = self.client.post(url, {}) self.assertContains(r, "Missing apikey parameter", status_code=400) - r = self.client.post(url, {'apikey': badapikey.hash()} ) - self.assertContains(r, "Restricted to role: Recording Manager", status_code=403) + r = self.client.post(url, {"apikey": badapikey.hash()}) + self.assertContains( + r, "Restricted to role: Recording Manager", status_code=403 + ) - r = self.client.get(url, {'apikey': apikey.hash()} ) + r = self.client.get(url, {"apikey": apikey.hash()}) self.assertContains(r, "Method not allowed", status_code=405) - r = self.client.post(url, {'apikey': apikey.hash()} ) + r = self.client.post(url, {"apikey": apikey.hash()}) self.assertContains(r, "Missing apidata parameter", status_code=400) for baddict in ( - '{}', + "{}", '{"bogons;drop table":"bogons;drop table"}', '{"session_id":"Not an integer;drop table"}', f'{{"session_id":{session.pk},"{type_id}":"not a list;drop table"}}', f'{{"session_id":{session.pk},"{type_id}":"not a list;drop table"}}', f'{{"session_id":{session.pk},"{type_id}":[{{}}, {{}}, "not an int;drop table", {{}}]}}', ): - r = self.client.post(url, {'apikey': apikey.hash(), 'apidata': baddict}) + r = self.client.post(url, {"apikey": apikey.hash(), "apidata": baddict}) self.assertContains(r, "Malformed post", status_code=400) - bad_session_id = Session.objects.order_by('-pk').first().pk + 1 - r = self.client.post(url, {'apikey': apikey.hash(), 'apidata': f'{{"session_id":{bad_session_id},"{type_id}":[]}}'}) + bad_session_id = Session.objects.order_by("-pk").first().pk + 1 + r = self.client.post( + url, + { + "apikey": apikey.hash(), + "apidata": f'{{"session_id":{bad_session_id},"{type_id}":[]}}', + }, + ) self.assertContains(r, "Invalid session", status_code=400) # Valid POST - r = self.client.post(url,{'apikey':apikey.hash(),'apidata': f'{{"session_id":{session.pk}, "{type_id}":{content}}}'}) + r = self.client.post( + url, + { + "apikey": apikey.hash(), + "apidata": f'{{"session_id":{session.pk}, "{type_id}":{content}}}', + }, + ) self.assertEqual(r.status_code, 200) newdoc = session.presentations.get(document__type_id=type_id).document - newdoccontent = get_unicode_document_content(newdoc.name, Path(session.meeting.get_materials_path()) / type_id / newdoc.uploaded_filename) + newdoccontent = get_unicode_document_content( + newdoc.name, + Path(session.meeting.get_materials_path()) + / type_id + / newdoc.uploaded_filename, + ) self.assertEqual(json.loads(content), json.loads(newdoccontent)) self.assertEqual( json.loads(retrieve_str(type_id, newdoc.uploaded_filename)), - json.loads(content) + json.loads(content), ) def test_api_upload_bluesheet(self): @@ -664,92 +804,126 @@ def test_api_upload_bluesheet(self): def test_person_export(self): person = PersonFactory() - url = urlreverse('ietf.api.views.PersonalInformationExportView') + url = urlreverse("ietf.api.views.PersonalInformationExportView") login_testing_unauthorized(self, person.user.username, url) r = self.client.get(url) self.assertEqual(r.status_code, 200) jsondata = r.json() - data = jsondata['person.person'][str(person.id)] - self.assertEqual(data['name'], person.name) - self.assertEqual(data['ascii'], person.ascii) - self.assertEqual(data['user']['email'], person.user.email) + data = jsondata["person.person"][str(person.id)] + self.assertEqual(data["name"], person.name) + self.assertEqual(data["ascii"], person.ascii) + self.assertEqual(data["user"]["email"], person.user.email) def test_api_v2_person_export_view(self): - url = urlreverse('ietf.api.views.ApiV2PersonExportView') + url = urlreverse("ietf.api.views.ApiV2PersonExportView") robot = PersonFactory(user__is_staff=True) - RoleFactory(name_id='robot', person=robot, email=robot.email(), group__acronym='secretariat') + RoleFactory( + name_id="robot", + person=robot, + email=robot.email(), + group__acronym="secretariat", + ) apikey = PersonalApiKeyFactory(endpoint=url, person=robot) # error cases r = self.client.post(url, {}) self.assertContains(r, "Missing apikey parameter", status_code=400) - badrole = RoleFactory(group__type_id='ietf', name_id='ad') + badrole = RoleFactory(group__type_id="ietf", name_id="ad") badapikey = PersonalApiKeyFactory(endpoint=url, person=badrole.person) badrole.person.user.last_login = timezone.now() badrole.person.user.save() - r = self.client.post(url, {'apikey': badapikey.hash()}) + r = self.client.post(url, {"apikey": badapikey.hash()}) self.assertContains(r, "Restricted to role: Robot", status_code=403) - r = self.client.post(url, {'apikey': apikey.hash()}) + r = self.client.post(url, {"apikey": apikey.hash()}) self.assertContains(r, "No filters provided", status_code=400) # working case - r = self.client.post(url, {'apikey': apikey.hash(), 'email': robot.email().address, '_expand': 'user'}) + r = self.client.post( + url, + { + "apikey": apikey.hash(), + "email": robot.email().address, + "_expand": "user", + }, + ) self.assertEqual(r.status_code, 200) jsondata = r.json() - data = jsondata['person.person'][str(robot.id)] - self.assertEqual(data['name'], robot.name) - self.assertEqual(data['ascii'], robot.ascii) - self.assertEqual(data['user']['email'], robot.user.email) + data = jsondata["person.person"][str(robot.id)] + self.assertEqual(data["name"], robot.name) + self.assertEqual(data["ascii"], robot.ascii) + self.assertEqual(data["user"]["email"], robot.user.email) def test_api_new_meeting_registration(self): - meeting = MeetingFactory(type_id='ietf') + meeting = MeetingFactory(type_id="ietf") reg = { - 'apikey': 'invalid', - 'affiliation': "Alguma Corporação", - 'country_code': 'PT', - 'email': 'foo@example.pt', - 'first_name': 'Foo', - 'last_name': 'Bar', - 'meeting': meeting.number, - 'reg_type': 'hackathon', - 'ticket_type': '', - 'checkedin': 'False', - 'is_nomcom_volunteer': 'False', + "apikey": "invalid", + "affiliation": "Alguma Corporação", + "country_code": "PT", + "email": "foo@example.pt", + "first_name": "Foo", + "last_name": "Bar", + "meeting": meeting.number, + "reg_type": "hackathon", + "ticket_type": "", + "checkedin": "False", + "is_nomcom_volunteer": "False", } - url = urlreverse('ietf.api.views.api_new_meeting_registration') + url = urlreverse("ietf.api.views.api_new_meeting_registration") r = self.client.post(url, reg) - self.assertContains(r, 'Invalid apikey', status_code=403) + self.assertContains(r, "Invalid apikey", status_code=403) oidcp = PersonFactory(user__is_staff=True) # Make sure 'oidcp' has an acceptable role - RoleFactory(name_id='robot', person=oidcp, email=oidcp.email(), group__acronym='secretariat') + RoleFactory( + name_id="robot", + person=oidcp, + email=oidcp.email(), + group__acronym="secretariat", + ) key = PersonalApiKeyFactory(person=oidcp, endpoint=url) - reg['apikey'] = key.hash() + reg["apikey"] = key.hash() # # Test valid POST # FIXME: sometimes, there seems to be something in the outbox? old_len = len(outbox) r = self.client.post(url, reg) - self.assertContains(r, "Accepted, New registration, Email sent", status_code=202) + self.assertContains( + r, "Accepted, New registration, Email sent", status_code=202 + ) # # Check outgoing mail self.assertEqual(len(outbox), old_len + 1) body = get_payload_text(outbox[-1]) - self.assertIn(reg['email'], outbox[-1]['To'] ) - self.assertIn(reg['email'], body) - self.assertIn('account creation request', body) + self.assertIn(reg["email"], outbox[-1]["To"]) + self.assertIn(reg["email"], body) + self.assertIn("account creation request", body) # # Check record - obj = MeetingRegistration.objects.get(email=reg['email'], meeting__number=reg['meeting']) - for key in ['affiliation', 'country_code', 'first_name', 'last_name', 'person', 'reg_type', 'ticket_type', 'checkedin']: - self.assertEqual(getattr(obj, key), False if key=='checkedin' else reg.get(key) , "Bad data for field '%s'" % key) + obj = MeetingRegistration.objects.get( + email=reg["email"], meeting__number=reg["meeting"] + ) + for key in [ + "affiliation", + "country_code", + "first_name", + "last_name", + "person", + "reg_type", + "ticket_type", + "checkedin", + ]: + self.assertEqual( + getattr(obj, key), + False if key == "checkedin" else reg.get(key), + "Bad data for field '%s'" % key, + ) # # Test with existing user person = PersonFactory() - reg['email'] = person.email().address - reg['first_name'] = person.first_name() - reg['last_name'] = person.last_name() + reg["email"] = person.email().address + reg["first_name"] = person.first_name() + reg["last_name"] = person.last_name() # r = self.client.post(url, reg) self.assertContains(r, "Accepted, New registration", status_code=202) @@ -758,58 +932,69 @@ def test_api_new_meeting_registration(self): self.assertEqual(len(outbox), old_len + 1) # # Test multiple reg types - reg['reg_type'] = 'remote' - reg['ticket_type'] = 'full_week_pass' + reg["reg_type"] = "remote" + reg["ticket_type"] = "full_week_pass" r = self.client.post(url, reg) self.assertContains(r, "Accepted, New registration", status_code=202) - objs = MeetingRegistration.objects.filter(email=reg['email'], meeting__number=reg['meeting']) + objs = MeetingRegistration.objects.filter( + email=reg["email"], meeting__number=reg["meeting"] + ) self.assertEqual(len(objs), 2) - self.assertEqual(objs.filter(reg_type='hackathon').count(), 1) - self.assertEqual(objs.filter(reg_type='remote', ticket_type='full_week_pass').count(), 1) + self.assertEqual(objs.filter(reg_type="hackathon").count(), 1) + self.assertEqual( + objs.filter(reg_type="remote", ticket_type="full_week_pass").count(), 1 + ) self.assertEqual(len(outbox), old_len + 1) # # Test incomplete POST - drop_fields = ['affiliation', 'first_name', 'reg_type'] + drop_fields = ["affiliation", "first_name", "reg_type"] for field in drop_fields: del reg[field] - r = self.client.post(url, reg) - self.assertContains(r, 'Missing parameters:', status_code=400) - err, fields = r.content.decode().split(':', 1) - missing_fields = [f.strip() for f in fields.split(',')] + r = self.client.post(url, reg) + self.assertContains(r, "Missing parameters:", status_code=400) + err, fields = r.content.decode().split(":", 1) + missing_fields = [f.strip() for f in fields.split(",")] self.assertEqual(set(missing_fields), set(drop_fields)) def test_api_new_meeting_registration_nomcom_volunteer(self): - '''Test that Volunteer is created if is_nomcom_volunteer=True - is submitted to API - ''' - meeting = MeetingFactory(type_id='ietf') + """Test that Volunteer is created if is_nomcom_volunteer=True + is submitted to API + """ + meeting = MeetingFactory(type_id="ietf") reg = { - 'apikey': 'invalid', - 'affiliation': "Alguma Corporação", - 'country_code': 'PT', - 'meeting': meeting.number, - 'reg_type': 'onsite', - 'ticket_type': '', - 'checkedin': 'False', - 'is_nomcom_volunteer': 'False', + "apikey": "invalid", + "affiliation": "Alguma Corporação", + "country_code": "PT", + "meeting": meeting.number, + "reg_type": "onsite", + "ticket_type": "", + "checkedin": "False", + "is_nomcom_volunteer": "False", } person = PersonFactory() - reg['email'] = person.email().address - reg['first_name'] = person.first_name() - reg['last_name'] = person.last_name() + reg["email"] = person.email().address + reg["first_name"] = person.first_name() + reg["last_name"] = person.last_name() now = datetime.datetime.now() if now.month > 10: year = now.year + 1 else: year = now.year # create appropriate group and nomcom objects - nomcom = NomComFactory.create(is_accepting_volunteers=True, **nomcom_kwargs_for_year(year)) - url = urlreverse('ietf.api.views.api_new_meeting_registration') + nomcom = NomComFactory.create( + is_accepting_volunteers=True, **nomcom_kwargs_for_year(year) + ) + url = urlreverse("ietf.api.views.api_new_meeting_registration") oidcp = PersonFactory(user__is_staff=True) # Make sure 'oidcp' has an acceptable role - RoleFactory(name_id='robot', person=oidcp, email=oidcp.email(), group__acronym='secretariat') + RoleFactory( + name_id="robot", + person=oidcp, + email=oidcp.email(), + group__acronym="secretariat", + ) key = PersonalApiKeyFactory(person=oidcp, endpoint=url) - reg['apikey'] = key.hash() + reg["apikey"] = key.hash() # first test is_nomcom_volunteer False r = self.client.post(url, reg) @@ -818,7 +1003,7 @@ def test_api_new_meeting_registration_nomcom_volunteer(self): self.assertEqual(Volunteer.objects.count(), 0) # test is_nomcom_volunteer True - reg['is_nomcom_volunteer'] = 'True' + reg["is_nomcom_volunteer"] = "True" r = self.client.post(url, reg) self.assertContains(r, "Accepted, Updated registration", status_code=202) # assert Volunteer exists @@ -826,46 +1011,52 @@ def test_api_new_meeting_registration_nomcom_volunteer(self): volunteer = Volunteer.objects.last() self.assertEqual(volunteer.person, person) self.assertEqual(volunteer.nomcom, nomcom) - self.assertEqual(volunteer.origin, 'registration') + self.assertEqual(volunteer.origin, "registration") def test_api_version(self): - DumpInfo.objects.create(date=timezone.datetime(2022,8,31,7,10,1,tzinfo=datetime.timezone.utc), host='testapi.example.com',tz='UTC') - url = urlreverse('ietf.api.views.version') + DumpInfo.objects.create( + date=timezone.datetime(2022, 8, 31, 7, 10, 1, tzinfo=datetime.timezone.utc), + host="testapi.example.com", + tz="UTC", + ) + url = urlreverse("ietf.api.views.version") r = self.client.get(url) data = r.json() - self.assertEqual(data['version'], ietf.__version__+ietf.__patch__) + self.assertEqual(data["version"], ietf.__version__ + ietf.__patch__) for lib in settings.ADVERTISE_VERSIONS: - self.assertIn(lib, data['other']) - self.assertEqual(data['dumptime'], "2022-08-31 07:10:01 +0000") - DumpInfo.objects.update(tz='PST8PDT') + self.assertIn(lib, data["other"]) + self.assertEqual(data["dumptime"], "2022-08-31 07:10:01 +0000") + DumpInfo.objects.update(tz="PST8PDT") r = self.client.get(url) - data = r.json() - self.assertEqual(data['dumptime'], "2022-08-31 07:10:01 -0700") - + data = r.json() + self.assertEqual(data["dumptime"], "2022-08-31 07:10:01 -0700") def test_api_appauth(self): for app in ["authortools", "bibxml"]: - url = urlreverse('ietf.api.views.app_auth', kwargs={"app": app}) + url = urlreverse("ietf.api.views.app_auth", kwargs={"app": app}) person = PersonFactory() apikey = PersonalApiKeyFactory(endpoint=url, person=person) - - self.client.login(username=person.user.username,password=f'{person.user.username}+password') + + self.client.login( + username=person.user.username, + password=f"{person.user.username}+password", + ) self.client.logout() - + # error cases # missing apikey r = self.client.post(url, {}) - self.assertContains(r, 'Missing apikey parameter', status_code=400) - + self.assertContains(r, "Missing apikey parameter", status_code=400) + # invalid apikey - r = self.client.post(url, {'apikey': 'foobar'}) - self.assertContains(r, 'Invalid apikey', status_code=403) - + r = self.client.post(url, {"apikey": "foobar"}) + self.assertContains(r, "Invalid apikey", status_code=403) + # working case - r = self.client.post(url, {'apikey': apikey.hash()}) + r = self.client.post(url, {"apikey": apikey.hash()}) self.assertEqual(r.status_code, 200) jsondata = r.json() - self.assertEqual(jsondata['success'], True) + self.assertEqual(jsondata["success"], True) self.client.logout() @override_settings(APP_API_TOKENS={"ietf.api.views.nfs_metrics": ["valid-token"]}) @@ -877,12 +1068,15 @@ def test_api_nfs_metrics(self): self.assertContains(r, 'nfs_latency_seconds{operation="write"}') def test_api_get_session_matherials_no_agenda_meeting_url(self): - meeting = MeetingFactory(type_id='ietf') + meeting = MeetingFactory(type_id="ietf") session = SessionFactory(meeting=meeting) - url = urlreverse('ietf.meeting.views.api_get_session_materials', kwargs={'session_id': session.pk}) + url = urlreverse( + "ietf.meeting.views.api_get_session_materials", + kwargs={"session_id": session.pk}, + ) r = self.client.get(url) self.assertEqual(r.status_code, 200) - + @override_settings(APP_API_TOKENS={"ietf.api.views.draft_aliases": ["valid-token"]}) @mock.patch("ietf.api.views.DraftAliasGenerator") def test_draft_aliases(self, mock): @@ -897,7 +1091,8 @@ def test_draft_aliases(self, mock): "aliases": [ {"alias": "alias1", "domains": ["ietf"], "addresses": ["a1", "a2"]}, {"alias": "alias2", "domains": ["ietf"], "addresses": ["a3", "a4"]}, - ]} + ] + }, ) # some invalid cases self.assertEqual( @@ -920,7 +1115,10 @@ def test_draft_aliases(self, mock): @override_settings(APP_API_TOKENS={"ietf.api.views.group_aliases": ["valid-token"]}) @mock.patch("ietf.api.views.GroupAliasGenerator") def test_group_aliases(self, mock): - mock.return_value = (("alias1", ("ietf",), ("a1", "a2")), ("alias2", ("ietf", "iab"), ("a3", "a4"))) + mock.return_value = ( + ("alias1", ("ietf",), ("a1", "a2")), + ("alias2", ("ietf", "iab"), ("a3", "a4")), + ) url = urlreverse("ietf.api.views.group_aliases") r = self.client.get(url, headers={"X-Api-Key": "valid-token"}) self.assertEqual(r.status_code, 200) @@ -930,8 +1128,13 @@ def test_group_aliases(self, mock): { "aliases": [ {"alias": "alias1", "domains": ["ietf"], "addresses": ["a1", "a2"]}, - {"alias": "alias2", "domains": ["ietf", "iab"], "addresses": ["a3", "a4"]}, - ]} + { + "alias": "alias2", + "domains": ["ietf", "iab"], + "addresses": ["a3", "a4"], + }, + ] + }, ) # some invalid cases self.assertEqual( @@ -951,7 +1154,9 @@ def test_group_aliases(self, mock): 405, ) - @override_settings(APP_API_TOKENS={"ietf.api.views.active_email_list": ["valid-token"]}) + @override_settings( + APP_API_TOKENS={"ietf.api.views.active_email_list": ["valid-token"]} + ) def test_active_email_list(self): EmailFactory(active=True) # make sure there's at least one active email... EmailFactory(active=False) # ... and at least one non-active emai @@ -971,9 +1176,14 @@ def test_active_email_list(self): self.assertEqual(r.headers["Content-Type"], "application/json") result = json.loads(r.content) self.assertCountEqual(result.keys(), ["addresses"]) - self.assertCountEqual(result["addresses"], Email.objects.filter(active=True).values_list("address", flat=True)) + self.assertCountEqual( + result["addresses"], + Email.objects.filter(active=True).values_list("address", flat=True), + ) - @override_settings(APP_API_TOKENS={"ietf.api.views.role_holder_addresses": ["valid-token"]}) + @override_settings( + APP_API_TOKENS={"ietf.api.views.role_holder_addresses": ["valid-token"]} + ) def test_role_holder_addresses(self): url = urlreverse("ietf.api.views.role_holder_addresses") r = self.client.get(url, headers={}) @@ -985,7 +1195,9 @@ def test_role_holder_addresses(self): emails = EmailFactory.create_batch(5) email_queryset = Email.objects.filter(pk__in=[e.pk for e in emails]) - with mock.patch("ietf.api.views.role_holder_emails", return_value=email_queryset): + with mock.patch( + "ietf.api.views.role_holder_emails", return_value=email_queryset + ): r = self.client.get(url, headers={"X-Api-Key": "valid-token"}) self.assertEqual(r.status_code, 200, "Good api token and method, access") content_dict = json.loads(r.content) @@ -996,16 +1208,17 @@ def test_role_holder_addresses(self): ) @override_settings( - APP_API_TOKENS={"ietf.api.views.ingest_email": "valid-token", "ietf.api.views.ingest_email_test": "test-token"} + APP_API_TOKENS={ + "ietf.api.views.ingest_email": "valid-token", + "ietf.api.views.ingest_email_test": "test-token", + } ) @mock.patch("ietf.api.views.iana_ingest_review_email") @mock.patch("ietf.api.views.ipr_ingest_response_email") @mock.patch("ietf.api.views.nomcom_ingest_feedback_email") - def test_ingest_email( - self, mock_nomcom_ingest, mock_ipr_ingest, mock_iana_ingest - ): + def test_ingest_email(self, mock_nomcom_ingest, mock_ipr_ingest, mock_iana_ingest): mocks = {mock_nomcom_ingest, mock_ipr_ingest, mock_iana_ingest} - empty_outbox() + empty_outbox() url = urlreverse("ietf.api.views.ingest_email") test_mode_url = urlreverse("ietf.api.views.ingest_email_test") @@ -1044,7 +1257,9 @@ def test_ingest_email( self.assertEqual(r.status_code, 400) self.assertFalse(any(m.called for m in mocks)) r = self.client.post( - test_mode_url, content_type="application/json", headers={"X-Api-Key": "test-token"} + test_mode_url, + content_type="application/json", + headers={"X-Api-Key": "test-token"}, ) self.assertEqual(r.status_code, 400) self.assertFalse(any(m.called for m in mocks)) @@ -1120,7 +1335,7 @@ def test_ingest_email( self.assertEqual(mock_iana_ingest.call_args, mock.call(b"This is a message")) self.assertFalse(any(m.called for m in (mocks - {mock_iana_ingest}))) mock_iana_ingest.reset_mock() - + # the test mode endpoint should _not_ call the handler r = self.client.post( test_mode_url, @@ -1133,7 +1348,7 @@ def test_ingest_email( self.assertEqual(json.loads(r.content), {"result": "ok"}) self.assertFalse(any(m.called for m in mocks)) mock_iana_ingest.reset_mock() - + r = self.client.post( url, {"dest": "ipr-response", "message": message_b64}, @@ -1201,7 +1416,9 @@ def test_ingest_email( self.assertEqual(r.headers["Content-Type"], "application/json") self.assertEqual(json.loads(r.content), {"result": "ok"}) self.assertTrue(mock_nomcom_ingest.called) - self.assertEqual(mock_nomcom_ingest.call_args, mock.call(b"This is a message", random_year)) + self.assertEqual( + mock_nomcom_ingest.call_args, mock.call(b"This is a message", random_year) + ) self.assertFalse(any(m.called for m in (mocks - {mock_nomcom_ingest}))) mock_nomcom_ingest.reset_mock() @@ -1232,14 +1449,16 @@ def test_ingest_email( self.assertTrue(mock_iana_ingest.called) self.assertEqual(mock_iana_ingest.call_args, mock.call(b"This is a message")) self.assertFalse(any(m.called for m in (mocks - {mock_iana_ingest}))) - self.assertEqual(len(outbox), 0) # implicitly tests that _none_ of the earlier tests sent email + self.assertEqual( + len(outbox), 0 + ) # implicitly tests that _none_ of the earlier tests sent email mock_iana_ingest.reset_mock() # test default recipients and attached original message mock_iana_ingest.side_effect = EmailIngestionError( "Error: do send email", email_body="This is my email\n", - email_original_message=b"This is the original message" + email_original_message=b"This is the original message", ) with override_settings(ADMINS=[("Some Admin", "admin@example.com")]): r = self.client.post( @@ -1270,7 +1489,7 @@ def test_ingest_email( mock_iana_ingest.side_effect = EmailIngestionError( "Error: do send email", email_body="This is my email\n", - email_recipients=("thatguy@example.com") + email_recipients=("thatguy@example.com"), ) with override_settings(ADMINS=[("Some Admin", "admin@example.com")]): r = self.client.post( @@ -1320,7 +1539,10 @@ def test_ingest_email( self.assertEqual(len(attachments), 1) self.assertEqual(attachments[0].get_filename(), "traceback.txt") self.assertEqual(attachments[0].get_content_type(), "text/plain") - self.assertIn("ietf.api.views.EmailIngestionError: Error: do send email", attachments[0].get_content()) + self.assertIn( + "ietf.api.views.EmailIngestionError: Error: do send email", + attachments[0].get_content(), + ) mock_iana_ingest.reset_mock() empty_outbox() @@ -1335,14 +1557,26 @@ def setUp(self): self.invalid_token = User.objects.make_random_password(20) self.url = urlreverse("ietf.api.views.directauth") self.valid_person = PersonFactory() - self.valid_password = self.valid_person.user.username+"+password" + self.valid_password = self.valid_person.user.username + "+password" self.invalid_password = self.valid_password while self.invalid_password == self.valid_password: self.invalid_password = User.objects.make_random_password(20) - self.valid_body_with_good_password = self.post_dict(authtoken=self.valid_token, username=self.valid_person.user.username, password=self.valid_password) - self.valid_body_with_bad_password = self.post_dict(authtoken=self.valid_token, username=self.valid_person.user.username, password=self.invalid_password) - self.valid_body_with_unknown_user = self.post_dict(authtoken=self.valid_token, username="notauser@nowhere.nada", password=self.valid_password) + self.valid_body_with_good_password = self.post_dict( + authtoken=self.valid_token, + username=self.valid_person.user.username, + password=self.valid_password, + ) + self.valid_body_with_bad_password = self.post_dict( + authtoken=self.valid_token, + username=self.valid_person.user.username, + password=self.invalid_password, + ) + self.valid_body_with_unknown_user = self.post_dict( + authtoken=self.valid_token, + username="notauser@nowhere.nada", + password=self.valid_password, + ) def post_dict(self, authtoken, username, password): data = dict() @@ -1352,7 +1586,7 @@ def post_dict(self, authtoken, username, password): data["username"] = username if password is not None: data["password"] = password - return dict(data = json.dumps(data)) + return dict(data=json.dumps(data)) def response_data(self, response): try: @@ -1363,18 +1597,36 @@ def response_data(self, response): return data def test_bad_methods(self): - for method in (self.client.get, self.client.put, self.client.head, self.client.delete, self.client.patch): + for method in ( + self.client.get, + self.client.put, + self.client.head, + self.client.delete, + self.client.patch, + ): r = method(self.url) self.assertEqual(r.status_code, 405) def test_bad_post(self): for bad in [ - self.post_dict(authtoken=None, username=self.valid_person.user.username, password=self.valid_password), - self.post_dict(authtoken=self.valid_token, username=None, password=self.valid_password), - self.post_dict(authtoken=self.valid_token, username=self.valid_person.user.username, password=None), + self.post_dict( + authtoken=None, + username=self.valid_person.user.username, + password=self.valid_password, + ), + self.post_dict( + authtoken=self.valid_token, username=None, password=self.valid_password + ), + self.post_dict( + authtoken=self.valid_token, + username=self.valid_person.user.username, + password=None, + ), self.post_dict(authtoken=None, username=None, password=self.valid_password), self.post_dict(authtoken=self.valid_token, username=None, password=None), - self.post_dict(authtoken=None, username=self.valid_person.user.username, password=None), + self.post_dict( + authtoken=None, username=self.valid_person.user.username, password=None + ), self.post_dict(authtoken=None, username=None, password=None), ]: r = self.client.post(self.url, bad) @@ -1382,23 +1634,29 @@ def test_bad_post(self): data = self.response_data(r) self.assertEqual(data["result"], "failure") self.assertEqual(data["reason"], "invalid post") - - bad = dict(authtoken=self.valid_token, username=self.valid_person.user.username, password=self.valid_password) + + bad = dict( + authtoken=self.valid_token, + username=self.valid_person.user.username, + password=self.valid_password, + ) r = self.client.post(self.url, bad) self.assertEqual(r.status_code, 200) data = self.response_data(r) self.assertEqual(data["result"], "failure") - self.assertEqual(data["reason"], "invalid post") + self.assertEqual(data["reason"], "invalid post") def test_notokenstore(self): self.assertFalse(hasattr(settings, "APP_API_TOKENS")) - r = self.client.post(self.url,self.valid_body_with_good_password) + r = self.client.post(self.url, self.valid_body_with_good_password) self.assertEqual(r.status_code, 200) data = self.response_data(r) self.assertEqual(data["result"], "failure") self.assertEqual(data["reason"], "invalid authtoken") - @override_settings(APP_API_TOKENS={"ietf.api.views.directauth":"nSZJDerbau6WZwbEAYuQ"}) + @override_settings( + APP_API_TOKENS={"ietf.api.views.directauth": "nSZJDerbau6WZwbEAYuQ"} + ) def test_bad_username(self): r = self.client.post(self.url, self.valid_body_with_unknown_user) self.assertEqual(r.status_code, 200) @@ -1406,7 +1664,9 @@ def test_bad_username(self): self.assertEqual(data["result"], "failure") self.assertEqual(data["reason"], "authentication failed") - @override_settings(APP_API_TOKENS={"ietf.api.views.directauth":"nSZJDerbau6WZwbEAYuQ"}) + @override_settings( + APP_API_TOKENS={"ietf.api.views.directauth": "nSZJDerbau6WZwbEAYuQ"} + ) def test_bad_password(self): r = self.client.post(self.url, self.valid_body_with_bad_password) self.assertEqual(r.status_code, 200) @@ -1414,47 +1674,55 @@ def test_bad_password(self): self.assertEqual(data["result"], "failure") self.assertEqual(data["reason"], "authentication failed") - @override_settings(APP_API_TOKENS={"ietf.api.views.directauth":"nSZJDerbau6WZwbEAYuQ"}) + @override_settings( + APP_API_TOKENS={"ietf.api.views.directauth": "nSZJDerbau6WZwbEAYuQ"} + ) def test_good_password(self): r = self.client.post(self.url, self.valid_body_with_good_password) self.assertEqual(r.status_code, 200) data = self.response_data(r) self.assertEqual(data["result"], "success") + class TastypieApiTestCase(ResourceTestCaseMixin, TestCase): def __init__(self, *args, **kwargs): self.apps = {} for app_name in settings.INSTALLED_APPS: - if app_name.startswith('ietf') and not app_name in OMITTED_APPS: + if app_name.startswith("ietf") and not app_name in OMITTED_APPS: app = import_module(app_name) - name = app_name.split('.',1)[-1] + name = app_name.split(".", 1)[-1] models_path = os.path.join(os.path.dirname(app.__file__), "models.py") if os.path.exists(models_path): self.apps[name] = app_name super(TastypieApiTestCase, self).__init__(*args, **kwargs) def test_api_top_level(self): - client = Client(Accept='application/json') + client = Client(Accept="application/json") r = client.get("/api/v1/") - self.assertValidJSONResponse(r) + self.assertValidJSONResponse(r) resource_list = r.json() for name in self.apps: if not name in resource_list: - sys.stderr.write("Expected a REST API resource for %s, but didn't find one\n" % name) + sys.stderr.write( + "Expected a REST API resource for %s, but didn't find one\n" % name + ) for name in self.apps: - self.assertIn(name, resource_list, - "Expected a REST API resource for %s, but didn't find one" % name) + self.assertIn( + name, + resource_list, + "Expected a REST API resource for %s, but didn't find one" % name, + ) def test_all_model_resources_exist(self): - client = Client(Accept='application/json') + client = Client(Accept="application/json") r = client.get("/api/v1") top = r.json() for name in self.apps: app_name = self.apps[name] app = import_module(app_name) - self.assertEqual("/api/v1/%s/"%name, top[name]["list_endpoint"]) + self.assertEqual("/api/v1/%s/" % name, top[name]["list_endpoint"]) r = client.get(top[name]["list_endpoint"]) self.assertValidJSONResponse(r) app_resources = r.json() @@ -1462,16 +1730,23 @@ def test_all_model_resources_exist(self): model_list = apps.get_app_config(name).get_models() for model in model_list: if not model._meta.model_name in list(app_resources.keys()): - #print("There doesn't seem to be any resource for model %s.models.%s"%(app.__name__,model.__name__,)) - self.assertIn(model._meta.model_name, list(app_resources.keys()), - "There doesn't seem to be any API resource for model %s.models.%s"%(app.__name__,model.__name__,)) + # print("There doesn't seem to be any resource for model %s.models.%s"%(app.__name__,model.__name__,)) + self.assertIn( + model._meta.model_name, + list(app_resources.keys()), + "There doesn't seem to be any API resource for model %s.models.%s" + % ( + app.__name__, + model.__name__, + ), + ) class RfcdiffSupportTests(TestCase): def setUp(self): super().setUp() - self.target_view = 'ietf.api.views.rfcdiff_latest_json' + self.target_view = "ietf.api.views.rfcdiff_latest_json" self._last_rfc_num = 8000 def getJson(self, view_args): @@ -1485,9 +1760,11 @@ def next_rfc_number(self): return self._last_rfc_num def do_draft_test(self, name): - draft = IndividualDraftFactory(name=name, rev='00', create_revisions=range(0,13)) + draft = IndividualDraftFactory( + name=name, rev="00", create_revisions=range(0, 13) + ) draft = reload_db_objects(draft) - prev_draft_rev = f'{(int(draft.rev)-1):02d}' + prev_draft_rev = f"{(int(draft.rev)-1):02d}" received = self.getJson(dict(name=draft.name)) self.assertEqual( @@ -1496,10 +1773,10 @@ def do_draft_test(self, name): name=draft.name, rev=draft.rev, content_url=draft.get_href(), - previous=f'{draft.name}-{prev_draft_rev}', - previous_url= draft.history_set.get(rev=prev_draft_rev).get_href(), + previous=f"{draft.name}-{prev_draft_rev}", + previous_url=draft.history_set.get(rev=prev_draft_rev).get_href(), ), - 'Incorrect JSON when draft revision not specified', + "Incorrect JSON when draft revision not specified", ) received = self.getJson(dict(name=draft.name, rev=draft.rev)) @@ -1509,64 +1786,77 @@ def do_draft_test(self, name): name=draft.name, rev=draft.rev, content_url=draft.get_href(), - previous=f'{draft.name}-{prev_draft_rev}', - previous_url= draft.history_set.get(rev=prev_draft_rev).get_href(), + previous=f"{draft.name}-{prev_draft_rev}", + previous_url=draft.history_set.get(rev=prev_draft_rev).get_href(), ), - 'Incorrect JSON when latest revision specified', + "Incorrect JSON when latest revision specified", ) - received = self.getJson(dict(name=draft.name, rev='10')) - prev_draft_rev = '09' + received = self.getJson(dict(name=draft.name, rev="10")) + prev_draft_rev = "09" self.assertEqual( received, dict( name=draft.name, - rev='10', - content_url=draft.history_set.get(rev='10').get_href(), - previous=f'{draft.name}-{prev_draft_rev}', - previous_url= draft.history_set.get(rev=prev_draft_rev).get_href(), + rev="10", + content_url=draft.history_set.get(rev="10").get_href(), + previous=f"{draft.name}-{prev_draft_rev}", + previous_url=draft.history_set.get(rev=prev_draft_rev).get_href(), ), - 'Incorrect JSON when historical revision specified', + "Incorrect JSON when historical revision specified", ) - received = self.getJson(dict(name=draft.name, rev='00')) - self.assertNotIn('previous', received, 'Rev 00 has no previous name when not replacing a draft') + received = self.getJson(dict(name=draft.name, rev="00")) + self.assertNotIn( + "previous", + received, + "Rev 00 has no previous name when not replacing a draft", + ) replaced = IndividualDraftFactory() - RelatedDocument.objects.create(relationship_id='replaces',source=draft,target=replaced) - received = self.getJson(dict(name=draft.name, rev='00')) - self.assertEqual(received['previous'], f'{replaced.name}-{replaced.rev}', - 'Rev 00 has a previous name when replacing a draft') + RelatedDocument.objects.create( + relationship_id="replaces", source=draft, target=replaced + ) + received = self.getJson(dict(name=draft.name, rev="00")) + self.assertEqual( + received["previous"], + f"{replaced.name}-{replaced.rev}", + "Rev 00 has a previous name when replacing a draft", + ) def test_draft(self): # test with typical, straightforward names - self.do_draft_test(name='draft-somebody-did-a-thing') + self.do_draft_test(name="draft-somebody-did-a-thing") # try with different potentially problematic names - self.do_draft_test(name='draft-someone-did-something-01-02') - self.do_draft_test(name='draft-someone-did-something-else-02') - self.do_draft_test(name='draft-someone-did-something-02-weird-01') + self.do_draft_test(name="draft-someone-did-something-01-02") + self.do_draft_test(name="draft-someone-did-something-else-02") + self.do_draft_test(name="draft-someone-did-something-02-weird-01") def do_draft_with_broken_history_test(self, name): - draft = IndividualDraftFactory(name=name, rev='10') - received = self.getJson(dict(name=draft.name,rev='09')) - self.assertEqual(received['rev'],'09') - self.assertEqual(received['previous'], f'{draft.name}-08') - self.assertTrue('warning' in received) + draft = IndividualDraftFactory(name=name, rev="10") + received = self.getJson(dict(name=draft.name, rev="09")) + self.assertEqual(received["rev"], "09") + self.assertEqual(received["previous"], f"{draft.name}-08") + self.assertTrue("warning" in received) def test_draft_with_broken_history(self): # test with typical, straightforward names - self.do_draft_with_broken_history_test(name='draft-somebody-did-something') + self.do_draft_with_broken_history_test(name="draft-somebody-did-something") # try with different potentially problematic names - self.do_draft_with_broken_history_test(name='draft-someone-did-something-01-02') - self.do_draft_with_broken_history_test(name='draft-someone-did-something-else-02') - self.do_draft_with_broken_history_test(name='draft-someone-did-something-02-weird-03') + self.do_draft_with_broken_history_test(name="draft-someone-did-something-01-02") + self.do_draft_with_broken_history_test( + name="draft-someone-did-something-else-02" + ) + self.do_draft_with_broken_history_test( + name="draft-someone-did-something-02-weird-03" + ) def do_rfc_test(self, draft_name): - draft = WgDraftFactory(name=draft_name, create_revisions=range(0,2)) + draft = WgDraftFactory(name=draft_name, create_revisions=range(0, 2)) rfc = WgRfcFactory(group=draft.group, rfc_number=self.next_rfc_number()) draft.relateddocument_set.create(relationship_id="became_rfc", target=rfc) - draft.set_state(State.objects.get(type_id='draft',slug='rfc')) - draft.set_state(State.objects.get(type_id='draft-iesg', slug='pub')) + draft.set_state(State.objects.get(type_id="draft", slug="rfc")) + draft.set_state(State.objects.get(type_id="draft-iesg", slug="pub")) draft, rfc = reload_db_objects(draft, rfc) number = rfc.rfc_number @@ -1576,61 +1866,75 @@ def do_rfc_test(self, draft_name): dict( content_url=rfc.get_href(), name=rfc.name, - previous=f'{draft.name}-{draft.rev}', - previous_url= draft.history_set.get(rev=draft.rev).get_href(), + previous=f"{draft.name}-{draft.rev}", + previous_url=draft.history_set.get(rev=draft.rev).get_href(), ), - 'Can look up an RFC by number', + "Can look up an RFC by number", ) num_received = received received = self.getJson(dict(name=rfc.name)) - self.assertEqual(num_received, received, 'RFC by canonical name gives same result as by number') + self.assertEqual( + num_received, + received, + "RFC by canonical name gives same result as by number", + ) - received = self.getJson(dict(name=f'RfC {number}')) - self.assertEqual(num_received, received, 'RFC with unusual spacing/caps gives same result as by number') + received = self.getJson(dict(name=f"RfC {number}")) + self.assertEqual( + num_received, + received, + "RFC with unusual spacing/caps gives same result as by number", + ) received = self.getJson(dict(name=draft.name)) - self.assertEqual(num_received, received, 'RFC by draft name and no rev gives same result as by number') + self.assertEqual( + num_received, + received, + "RFC by draft name and no rev gives same result as by number", + ) - received = self.getJson(dict(name=draft.name, rev='01')) - prev_draft_rev = '00' + received = self.getJson(dict(name=draft.name, rev="01")) + prev_draft_rev = "00" self.assertEqual( received, dict( - content_url=draft.history_set.get(rev='01').get_href(), + content_url=draft.history_set.get(rev="01").get_href(), name=draft.name, - rev='01', - previous=f'{draft.name}-{prev_draft_rev}', - previous_url= draft.history_set.get(rev=prev_draft_rev).get_href(), + rev="01", + previous=f"{draft.name}-{prev_draft_rev}", + previous_url=draft.history_set.get(rev=prev_draft_rev).get_href(), ), - 'RFC by draft name with rev should give draft name, not canonical name' + "RFC by draft name with rev should give draft name, not canonical name", ) def test_rfc(self): # simple draft name - self.do_rfc_test(draft_name='draft-test-ar-ef-see') + self.do_rfc_test(draft_name="draft-test-ar-ef-see") # tricky draft names - self.do_rfc_test(draft_name='draft-whatever-02') - self.do_rfc_test(draft_name='draft-test-me-03-04') + self.do_rfc_test(draft_name="draft-whatever-02") + self.do_rfc_test(draft_name="draft-test-me-03-04") def test_rfc_with_tombstone(self): - draft = WgDraftFactory(create_revisions=range(0,2)) - rfc = WgRfcFactory(rfc_number=3261,group=draft.group)# See views_doc.HAS_TOMBSTONE + draft = WgDraftFactory(create_revisions=range(0, 2)) + rfc = WgRfcFactory( + rfc_number=3261, group=draft.group + ) # See views_doc.HAS_TOMBSTONE draft.relateddocument_set.create(relationship_id="became_rfc", target=rfc) - draft.set_state(State.objects.get(type_id='draft',slug='rfc')) - draft.set_state(State.objects.get(type_id='draft-iesg', slug='pub')) + draft.set_state(State.objects.get(type_id="draft", slug="rfc")) + draft.set_state(State.objects.get(type_id="draft-iesg", slug="pub")) draft = reload_db_objects(draft) # Some old rfcs had tombstones that shouldn't be used for comparisons received = self.getJson(dict(name=rfc.name)) - self.assertTrue(received['previous'].endswith('00')) + self.assertTrue(received["previous"].endswith("00")) def do_rfc_with_broken_history_test(self, draft_name): - draft = WgDraftFactory(rev='10', name=draft_name) + draft = WgDraftFactory(rev="10", name=draft_name) rfc = WgRfcFactory(group=draft.group, rfc_number=self.next_rfc_number()) draft.relateddocument_set.create(relationship_id="became_rfc", target=rfc) - draft.set_state(State.objects.get(type_id='draft',slug='rfc')) - draft.set_state(State.objects.get(type_id='draft-iesg', slug='pub')) + draft.set_state(State.objects.get(type_id="draft", slug="rfc")) + draft.set_state(State.objects.get(type_id="draft-iesg", slug="pub")) draft = reload_db_objects(draft) received = self.getJson(dict(name=draft.name)) @@ -1639,46 +1943,76 @@ def do_rfc_with_broken_history_test(self, draft_name): dict( content_url=rfc.get_href(), name=rfc.name, - previous=f'{draft.name}-10', - previous_url= f'{settings.IETF_ID_ARCHIVE_URL}{draft.name}-10.txt', + previous=f"{draft.name}-10", + previous_url=f"{settings.IETF_ID_ARCHIVE_URL}{draft.name}-10.txt", ), - 'RFC by draft name without rev should return canonical RFC name and no rev', + "RFC by draft name without rev should return canonical RFC name and no rev", ) - received = self.getJson(dict(name=draft.name, rev='10')) - self.assertEqual(received['name'], draft.name, 'RFC by draft name with rev should return draft name') - self.assertEqual(received['rev'], '10', 'Requested rev should be returned') - self.assertEqual(received['previous'], f'{draft.name}-09', 'Previous rev is one less than requested') - self.assertIn(f'{draft.name}-10', received['content_url'], 'Returned URL should include requested rev') - self.assertNotIn('warning', received, 'No warning when we have the rev requested') + received = self.getJson(dict(name=draft.name, rev="10")) + self.assertEqual( + received["name"], + draft.name, + "RFC by draft name with rev should return draft name", + ) + self.assertEqual(received["rev"], "10", "Requested rev should be returned") + self.assertEqual( + received["previous"], + f"{draft.name}-09", + "Previous rev is one less than requested", + ) + self.assertIn( + f"{draft.name}-10", + received["content_url"], + "Returned URL should include requested rev", + ) + self.assertNotIn( + "warning", received, "No warning when we have the rev requested" + ) - received = self.getJson(dict(name=f'{draft.name}-09')) - self.assertEqual(received['name'], draft.name, 'RFC by draft name with rev should return draft name') - self.assertEqual(received['rev'], '09', 'Requested rev should be returned') - self.assertEqual(received['previous'], f'{draft.name}-08', 'Previous rev is one less than requested') - self.assertIn(f'{draft.name}-09', received['content_url'], 'Returned URL should include requested rev') + received = self.getJson(dict(name=f"{draft.name}-09")) + self.assertEqual( + received["name"], + draft.name, + "RFC by draft name with rev should return draft name", + ) + self.assertEqual(received["rev"], "09", "Requested rev should be returned") + self.assertEqual( + received["previous"], + f"{draft.name}-08", + "Previous rev is one less than requested", + ) + self.assertIn( + f"{draft.name}-09", + received["content_url"], + "Returned URL should include requested rev", + ) self.assertEqual( - received['warning'], - 'History for this version not found - these results are speculation', - 'Warning should be issued when requested rev is not found' + received["warning"], + "History for this version not found - these results are speculation", + "Warning should be issued when requested rev is not found", ) def test_rfc_with_broken_history(self): # simple draft name - self.do_rfc_with_broken_history_test(draft_name='draft-some-draft') + self.do_rfc_with_broken_history_test(draft_name="draft-some-draft") # tricky draft names - self.do_rfc_with_broken_history_test(draft_name='draft-gizmo-01') - self.do_rfc_with_broken_history_test(draft_name='draft-oh-boy-what-a-draft-02-03') + self.do_rfc_with_broken_history_test(draft_name="draft-gizmo-01") + self.do_rfc_with_broken_history_test( + draft_name="draft-oh-boy-what-a-draft-02-03" + ) def test_no_such_document(self): - for name in ['rfc0000', 'draft-ftei-oof-rab-00']: - url = urlreverse(self.target_view, kwargs={'name': name}) + for name in ["rfc0000", "draft-ftei-oof-rab-00"]: + url = urlreverse(self.target_view, kwargs={"name": name}) r = self.client.get(url) self.assertEqual(r.status_code, 404) class TokenTests(TestCase): - @override_settings(APP_API_TOKENS={"known.endpoint": ["token in a list"], "oops": "token as a str"}) + @override_settings( + APP_API_TOKENS={"known.endpoint": ["token in a list"], "oops": "token as a str"} + ) def test_is_valid_token(self): # various invalid cases self.assertFalse(is_valid_token("unknown.endpoint", "token in a list")) @@ -1699,7 +2033,7 @@ def fn_to_wrap(request, *args, **kwargs): nonlocal called called = True return request, args, kwargs - + req_factory = RequestFactory() arg = object() kwarg = object() @@ -1718,13 +2052,13 @@ def fn_to_wrap(request, *args, **kwargs): # Bad X-Api-Key header (not resetting the mock, it was not used yet) val = fn_to_wrap( req_factory.get("/some/url", headers={"X-Api-Key": "some-value"}), - arg, + arg, kwarg=kwarg, ) self.assertTrue(isinstance(val, HttpResponseForbidden)) self.assertTrue(mock_is_valid_token.called) self.assertEqual( - mock_is_valid_token.call_args[0], + mock_is_valid_token.call_args[0], (fn_to_wrap.__module__ + "." + fn_to_wrap.__qualname__, "some-value"), ) self.assertFalse(called) @@ -1732,17 +2066,17 @@ def fn_to_wrap(request, *args, **kwargs): # Valid header mock_is_valid_token.reset_mock() mock_is_valid_token.return_value = True - request = req_factory.get("/some/url", headers={"X-Api-Key": "some-value"}) + request = req_factory.get("/some/url", headers={"X-Api-Key": "some-value"}) # Bad X-Api-Key header (not resetting the mock, it was not used yet) val = fn_to_wrap( request, - arg, + arg, kwarg=kwarg, ) self.assertEqual(val, (request, (arg,), {"kwarg": kwarg})) self.assertTrue(mock_is_valid_token.called) self.assertEqual( - mock_is_valid_token.call_args[0], + mock_is_valid_token.call_args[0], (fn_to_wrap.__module__ + "." + fn_to_wrap.__qualname__, "some-value"), ) self.assertTrue(called) @@ -1751,9 +2085,9 @@ def fn_to_wrap(request, *args, **kwargs): @requires_api_token("endpoint") def another_fn_to_wrap(request): return "yep" - + val = another_fn_to_wrap(request) self.assertEqual( - mock_is_valid_token.call_args[0], + mock_is_valid_token.call_args[0], ("endpoint", "some-value"), ) diff --git a/ietf/api/tests_core.py b/ietf/api/tests_core.py index 7e45deac8a..79b2605a0e 100644 --- a/ietf/api/tests_core.py +++ b/ietf/api/tests_core.py @@ -1,6 +1,7 @@ # Copyright The IETF Trust 2024, All Rights Reserved """Core API tests""" from unittest.mock import patch + # from unittest.mock import patch, call from django.urls import reverse as urlreverse, NoReverseMatch @@ -17,9 +18,11 @@ class CoreApiTestCase(TestCase): class PersonTests(CoreApiTestCase): # Tests disabled until we activate the DRF URLs in api/urls.py - + def test_person_detail(self): - with self.assertRaises(NoReverseMatch, msg="Re-enable test when this view is enabled"): + with self.assertRaises( + NoReverseMatch, msg="Re-enable test when this view is enabled" + ): urlreverse("ietf.api.core_api.person-detail", kwargs={"pk": 1}) # person = PersonFactory() @@ -70,19 +73,21 @@ def test_person_detail(self): @patch("ietf.person.api.send_new_email_confirmation_request") def test_add_email(self, send_confirmation_mock): - with self.assertRaises(NoReverseMatch, msg="Re-enable this test when this view is enabled"): + with self.assertRaises( + NoReverseMatch, msg="Re-enable this test when this view is enabled" + ): urlreverse("ietf.api.core_api.person-email", kwargs={"pk": 1}) - + # email = EmailFactory(address="old@example.org") # person = email.person # other_person = PersonFactory() # url = urlreverse("ietf.api.core_api.person-email", kwargs={"pk": person.pk}) # post_data = {"address": "new@example.org"} - # + # # r = self.client.post(url, data=post_data, format="json") # self.assertEqual(r.status_code, 403, "Must be logged in") # self.assertFalse(send_confirmation_mock.called) - # + # # self.client.login( # username=other_person.user.username, # password=other_person.user.username + "+password", @@ -90,7 +95,7 @@ def test_add_email(self, send_confirmation_mock): # r = self.client.post(url, data=post_data, format="json") # self.assertEqual(r.status_code, 403, "Can only retrieve self") # self.assertFalse(send_confirmation_mock.called) - # + # # self.client.login( # username=person.user.username, password=person.user.username + "+password" # ) @@ -105,7 +110,9 @@ def test_add_email(self, send_confirmation_mock): class EmailTests(CoreApiTestCase): def test_email_update(self): - with self.assertRaises(NoReverseMatch, msg="Re-enable this test when the view is enabled"): + with self.assertRaises( + NoReverseMatch, msg="Re-enable this test when the view is enabled" + ): urlreverse( "ietf.api.core_api.email-detail", kwargs={"pk": "original@example.org"} ) @@ -122,14 +129,14 @@ def test_email_update(self): # "ietf.api.core_api.email-detail", # kwargs={"pk": "not-original@example.org"}, # ) - # + # # r = self.client.put( # bad_url, data={"primary": True, "active": False}, format="json" # ) # self.assertEqual(r.status_code, 403, "Must be logged in preferred to 404") # r = self.client.put(url, data={"primary": True, "active": False}, format="json") # self.assertEqual(r.status_code, 403, "Must be logged in") - # + # # self.client.login( # username=other_person.user.username, # password=other_person.user.username + "+password", @@ -140,7 +147,7 @@ def test_email_update(self): # self.assertEqual(r.status_code, 404, "No such address") # r = self.client.put(url, data={"primary": True, "active": False}, format="json") # self.assertEqual(r.status_code, 403, "Can only access own addresses") - # + # # self.client.login( # username=person.user.username, password=person.user.username + "+password" # ) @@ -162,7 +169,7 @@ def test_email_update(self): # self.assertTrue(email.primary) # self.assertFalse(email.active) # self.assertEqual(email.origin, "factory") - # + # # # address / origin should be immutable # r = self.client.put( # url, @@ -193,7 +200,9 @@ def test_email_update(self): # self.assertEqual(email.origin, "factory") def test_email_partial_update(self): - with self.assertRaises(NoReverseMatch, msg="Re-enable this test when the view is enabled"): + with self.assertRaises( + NoReverseMatch, msg="Re-enable this test when the view is enabled" + ): urlreverse( "ietf.api.core_api.email-detail", kwargs={"pk": "original@example.org"} ) @@ -210,14 +219,14 @@ def test_email_partial_update(self): # "ietf.api.core_api.email-detail", # kwargs={"pk": "not-original@example.org"}, # ) - # + # # r = self.client.patch( # bad_url, data={"primary": True}, format="json" # ) # self.assertEqual(r.status_code, 403, "Must be logged in preferred to 404") # r = self.client.patch(url, data={"primary": True}, format="json") # self.assertEqual(r.status_code, 403, "Must be logged in") - # + # # self.client.login( # username=other_person.user.username, # password=other_person.user.username + "+password", @@ -228,7 +237,7 @@ def test_email_partial_update(self): # self.assertEqual(r.status_code, 404, "No such address") # r = self.client.patch(url, data={"primary": True}, format="json") # self.assertEqual(r.status_code, 403, "Can only access own addresses") - # + # # self.client.login( # username=person.user.username, password=person.user.username + "+password" # ) @@ -250,7 +259,7 @@ def test_email_partial_update(self): # self.assertTrue(email.primary) # self.assertTrue(email.active) # self.assertEqual(email.origin, "factory") - # + # # r = self.client.patch(url, data={"active": False}, format="json") # self.assertEqual(r.status_code, 200) # self.assertEqual( @@ -269,7 +278,7 @@ def test_email_partial_update(self): # self.assertTrue(email.primary) # self.assertFalse(email.active) # self.assertEqual(email.origin, "factory") - # + # # r = self.client.patch(url, data={"address": "modified@example.org"}, format="json") # self.assertEqual(r.status_code, 200) # extra fields allowed, but ignored # email.refresh_from_db() @@ -278,7 +287,7 @@ def test_email_partial_update(self): # self.assertTrue(email.primary) # self.assertFalse(email.active) # self.assertEqual(email.origin, "factory") - # + # # r = self.client.patch(url, data={"origin": "hacker"}, format="json") # self.assertEqual(r.status_code, 200) # extra fields allowed, but ignored # email.refresh_from_db() diff --git a/ietf/api/urls.py b/ietf/api/urls.py index b0dbaf91ce..baaa4d2977 100644 --- a/ietf/api/urls.py +++ b/ietf/api/urls.py @@ -16,7 +16,7 @@ # from drf_spectacular.views import SpectacularAPIView # from django.urls import path # from ietf.person import api as person_api -# from .routers import PrefixedSimpleRouter +# from .routers import PrefixedSimpleRouter # core_router = PrefixedSimpleRouter(name_prefix="ietf.api.core_api") # core api router # core_router.register("email", person_api.EmailViewSet) # core_router.register("person", person_api.PersonViewSet) @@ -25,75 +25,100 @@ urlpatterns = [ # General API help page - url(r'^$', api_views.api_help), + url(r"^$", api_views.api_help), # Top endpoint for Tastypie's REST API (this isn't standard Tastypie): - url(r'^v1/?$', api_views.top_level), + url(r"^v1/?$", api_views.top_level), # For mailarchive use, requires secretariat role - url(r'^v2/person/person', api_views.ApiV2PersonExportView.as_view()), + url(r"^v2/person/person", api_views.ApiV2PersonExportView.as_view()), # --- DRF API --- # path("core/", include(core_router.urls)), # path("schema/", SpectacularAPIView.as_view()), # # --- Custom API endpoints, sorted alphabetically --- # Email alias information for drafts - url(r'^doc/draft-aliases/$', api_views.draft_aliases), + url(r"^doc/draft-aliases/$", api_views.draft_aliases), # email ingestor - url(r'email/$', api_views.ingest_email), + url(r"email/$", api_views.ingest_email), # email ingestor - url(r'email/test/$', api_views.ingest_email_test), + url(r"email/test/$", api_views.ingest_email_test), # GDPR: export of personal information for the logged-in person - url(r'^export/personal-information/$', api_views.PersonalInformationExportView.as_view()), + url( + r"^export/personal-information/$", + api_views.PersonalInformationExportView.as_view(), + ), # Email alias information for groups - url(r'^group/group-aliases/$', api_views.group_aliases), + url(r"^group/group-aliases/$", api_views.group_aliases), # Email addresses belonging to role holders - url(r'^group/role-holder-addresses/$', api_views.role_holder_addresses), + url(r"^group/role-holder-addresses/$", api_views.role_holder_addresses), # Let IESG members set positions programmatically - url(r'^iesg/position', views_ballot.api_set_position), + url(r"^iesg/position", views_ballot.api_set_position), # Let Meetecho set session video URLs - url(r'^meeting/session/video/url$', meeting_views.api_set_session_video_url), + url(r"^meeting/session/video/url$", meeting_views.api_set_session_video_url), # Let Meetecho tell us the name of its recordings - url(r'^meeting/session/recording-name$', meeting_views.api_set_meetecho_recording_name), + url( + r"^meeting/session/recording-name$", + meeting_views.api_set_meetecho_recording_name, + ), # Meeting agenda + floorplan data - url(r'^meeting/(?P[A-Za-z0-9._+-]+)/agenda-data$', meeting_views.api_get_agenda_data), + url( + r"^meeting/(?P[A-Za-z0-9._+-]+)/agenda-data$", + meeting_views.api_get_agenda_data, + ), # Meeting session materials - url(r'^meeting/session/(?P[A-Za-z0-9._+-]+)/materials$', meeting_views.api_get_session_materials), + url( + r"^meeting/session/(?P[A-Za-z0-9._+-]+)/materials$", + meeting_views.api_get_session_materials, + ), # Let MeetEcho upload bluesheets - url(r'^notify/meeting/bluesheet/?$', meeting_views.api_upload_bluesheet), + url(r"^notify/meeting/bluesheet/?$", meeting_views.api_upload_bluesheet), # Let MeetEcho tell us about session attendees - url(r'^notify/session/attendees/?$', meeting_views.api_add_session_attendees), + url(r"^notify/session/attendees/?$", meeting_views.api_add_session_attendees), # Let MeetEcho upload session chatlog - url(r'^notify/session/chatlog/?$', meeting_views.api_upload_chatlog), + url(r"^notify/session/chatlog/?$", meeting_views.api_upload_chatlog), # Let MeetEcho upload session polls - url(r'^notify/session/polls/?$', meeting_views.api_upload_polls), + url(r"^notify/session/polls/?$", meeting_views.api_upload_polls), # Let the registration system notify us about registrations - url(r'^notify/meeting/registration/?', api_views.api_new_meeting_registration), + url(r"^notify/meeting/registration/?", api_views.api_new_meeting_registration), # OpenID authentication provider - url(r'^openid/$', TemplateView.as_view(template_name='api/openid-issuer.html'), name='ietf.api.urls.oidc_issuer'), - url(r'^openid/', include('oidc_provider.urls', namespace='oidc_provider')), + url( + r"^openid/$", + TemplateView.as_view(template_name="api/openid-issuer.html"), + name="ietf.api.urls.oidc_issuer", + ), + url(r"^openid/", include("oidc_provider.urls", namespace="oidc_provider")), # Email alias listing - url(r'^person/email/$', api_views.active_email_list), + url(r"^person/email/$", api_views.active_email_list), # Draft submission API - url(r'^submit/?$', submit_views.api_submit_tombstone), + url(r"^submit/?$", submit_views.api_submit_tombstone), # Draft upload API - url(r'^submission/?$', submit_views.api_submission), + url(r"^submission/?$", submit_views.api_submission), # Draft submission state API - url(r'^submission/(?P[0-9]+)/status/?', submit_views.api_submission_status), + url( + r"^submission/(?P[0-9]+)/status/?", + submit_views.api_submission_status, + ), # Datatracker version - url(r'^version/?$', api_views.version), + url(r"^version/?$", api_views.version), # Application authentication API key - url(r'^appauth/(?Pauthortools|bibxml)$', api_views.app_auth), + url(r"^appauth/(?Pauthortools|bibxml)$", api_views.app_auth), # NFS metrics endpoint - url(r'^metrics/nfs/?$', api_views.nfs_metrics), + url(r"^metrics/nfs/?$", api_views.nfs_metrics), # latest versions - url(r'^rfcdiff-latest-json/%(name)s(?:-%(rev)s)?(\.txt|\.html)?/?$' % settings.URL_REGEXPS, api_views.rfcdiff_latest_json), - url(r'^rfcdiff-latest-json/(?P[Rr][Ff][Cc] [0-9]+?)(\.txt|\.html)?/?$', api_views.rfcdiff_latest_json), + url( + r"^rfcdiff-latest-json/%(name)s(?:-%(rev)s)?(\.txt|\.html)?/?$" + % settings.URL_REGEXPS, + api_views.rfcdiff_latest_json, + ), + url( + r"^rfcdiff-latest-json/(?P[Rr][Ff][Cc] [0-9]+?)(\.txt|\.html)?/?$", + api_views.rfcdiff_latest_json, + ), # direct authentication - url(r'^directauth/?$', api_views.directauth), + url(r"^directauth/?$", api_views.directauth), ] # Additional (standard) Tastypie endpoints -for n,a in api._api_list: +for n, a in api._api_list: urlpatterns += [ - url(r'^v1/', include(a.urls)), + url(r"^v1/", include(a.urls)), ] - diff --git a/ietf/api/views.py b/ietf/api/views.py index 2fd9d2730f..dcb9f53eda 100644 --- a/ietf/api/views.py +++ b/ietf/api/views.py @@ -60,11 +60,11 @@ def top_level(request): available_resources = {} - apitop = reverse('ietf.api.views.top_level') + apitop = reverse("ietf.api.views.top_level") - for name in sorted([ name for name, api in _api_list if len(api._registry) > 0 ]): + for name in sorted([name for name, api in _api_list if len(api._registry) > 0]): available_resources[name] = { - 'list_endpoint': '%s/%s/' % (apitop, name), + "list_endpoint": "%s/%s/" % (apitop, name), } serializer = Serializer() @@ -72,68 +72,113 @@ def top_level(request): options = {} - if 'text/javascript' in desired_format: - callback = request.GET.get('callback', 'callback') + if "text/javascript" in desired_format: + callback = request.GET.get("callback", "callback") if not is_valid_jsonp_callback_value(callback): - raise BadRequest('JSONP callback name is invalid.') + raise BadRequest("JSONP callback name is invalid.") - options['callback'] = callback + options["callback"] = callback serialized = serializer.serialize(available_resources, desired_format, options) - return HttpResponse(content=serialized, content_type=build_content_type(desired_format)) + return HttpResponse( + content=serialized, content_type=build_content_type(desired_format) + ) + def api_help(request): key = JWK() # import just public part here, for display in info page key.import_from_pem(settings.API_PUBLIC_KEY_PEM) - return render(request, "api/index.html", {'key': key, 'settings':settings, }) - + return render( + request, + "api/index.html", + { + "key": key, + "settings": settings, + }, + ) + -@method_decorator((login_required, gzip_page), name='dispatch') +@method_decorator((login_required, gzip_page), name="dispatch") class PersonalInformationExportView(DetailView, JsonExportMixin): model = Person def get(self, request): person = get_object_or_404(self.model, user=request.user) - expand = ['searchrule', 'documentauthor', 'ad_document_set', 'ad_dochistory_set', 'docevent', - 'ballotpositiondocevent', 'deletedevent', 'email_set', 'groupevent', 'role', 'rolehistory', 'iprdisclosurebase', - 'iprevent', 'liaisonstatementevent', 'allowlisted', 'schedule', 'constraint', 'schedulingevent', 'message', - 'sendqueue', 'nominee', 'topicfeedbacklastseen', 'alias', 'email', 'apikeys', 'personevent', - 'reviewersettings', 'reviewsecretarysettings', 'unavailableperiod', 'reviewwish', - 'nextreviewerinteam', 'reviewrequest', 'meetingregistration', 'submissionevent', 'preapproval', - 'user', 'communitylist', 'personextresource_set', ] - - - return self.json_view(request, filter={'id':person.id}, expand=expand) - - -@method_decorator((csrf_exempt, require_api_key, role_required('Robot')), name='dispatch') + expand = [ + "searchrule", + "documentauthor", + "ad_document_set", + "ad_dochistory_set", + "docevent", + "ballotpositiondocevent", + "deletedevent", + "email_set", + "groupevent", + "role", + "rolehistory", + "iprdisclosurebase", + "iprevent", + "liaisonstatementevent", + "allowlisted", + "schedule", + "constraint", + "schedulingevent", + "message", + "sendqueue", + "nominee", + "topicfeedbacklastseen", + "alias", + "email", + "apikeys", + "personevent", + "reviewersettings", + "reviewsecretarysettings", + "unavailableperiod", + "reviewwish", + "nextreviewerinteam", + "reviewrequest", + "meetingregistration", + "submissionevent", + "preapproval", + "user", + "communitylist", + "personextresource_set", + ] + + return self.json_view(request, filter={"id": person.id}, expand=expand) + + +@method_decorator( + (csrf_exempt, require_api_key, role_required("Robot")), name="dispatch" +) class ApiV2PersonExportView(DetailView, JsonExportMixin): model = Person def err(self, code, text): - return HttpResponse(text, status=code, content_type='text/plain') + return HttpResponse(text, status=code, content_type="text/plain") def post(self, request): querydict = request.POST.copy() - querydict.pop('apikey', None) - expand = querydict.pop('_expand', []) + querydict.pop("apikey", None) + expand = querydict.pop("_expand", []) if not querydict: return self.err(400, "No filters provided") return self.json_view(request, filter=querydict.dict(), expand=expand) + # @require_api_key # @csrf_exempt # def person_access_token(request): # person = get_object_or_404(Person, user=request.user) -# +# # if request.method == 'POST': # client_id = request.POST.get('client_id', None) # client_secret = request.POST.get('client_secret', None) # client = get_object_or_404(ClientRecord, client_id=client_id, client_secret=client_secret) -# +# # return HttpResponse(json.dumps({ # 'name' : person.plain_name(), # 'email': person.email().address, @@ -145,17 +190,30 @@ def post(self, request): # else: # return HttpResponse(status=405) + @require_api_key -@role_required('Robot') +@role_required("Robot") @csrf_exempt def api_new_meeting_registration(request): - '''REST API to notify the datatracker about a new meeting registration''' + """REST API to notify the datatracker about a new meeting registration""" + def err(code, text): - return HttpResponse(text, status=code, content_type='text/plain') - required_fields = [ 'meeting', 'first_name', 'last_name', 'affiliation', 'country_code', - 'email', 'reg_type', 'ticket_type', 'checkedin', 'is_nomcom_volunteer'] + return HttpResponse(text, status=code, content_type="text/plain") + + required_fields = [ + "meeting", + "first_name", + "last_name", + "affiliation", + "country_code", + "email", + "reg_type", + "ticket_type", + "checkedin", + "is_nomcom_volunteer", + ] fields = required_fields + [] - if request.method == 'POST': + if request.method == "POST": # parameters: # apikey: # meeting @@ -163,8 +221,10 @@ def err(code, text): # email # reg_type (In Person, Remote, Hackathon Only) # ticket_type (full_week, one_day, student) - # - data = {'attended': False, } + # + data = { + "attended": False, + } missing_fields = [] for item in fields: value = request.POST.get(item, None) @@ -172,34 +232,34 @@ def err(code, text): missing_fields.append(item) data[item] = value if missing_fields: - return err(400, "Missing parameters: %s" % ', '.join(missing_fields)) - number = data['meeting'] + return err(400, "Missing parameters: %s" % ", ".join(missing_fields)) + number = data["meeting"] try: meeting = Meeting.objects.get(number=number) except Meeting.DoesNotExist: - return err(400, "Invalid meeting value: '%s'" % (number, )) - reg_type = data['reg_type'] - email = data['email'] + return err(400, "Invalid meeting value: '%s'" % (number,)) + reg_type = data["reg_type"] + email = data["email"] try: validate_email(email) except ValidationError: - return err(400, "Invalid email value: '%s'" % (email, )) - if request.POST.get('cancelled', 'false') == 'true': + return err(400, "Invalid email value: '%s'" % (email,)) + if request.POST.get("cancelled", "false") == "true": MeetingRegistration.objects.filter( - meeting_id=meeting.pk, - email=email, - reg_type=reg_type).delete() - return HttpResponse('OK', status=200, content_type='text/plain') + meeting_id=meeting.pk, email=email, reg_type=reg_type + ).delete() + return HttpResponse("OK", status=200, content_type="text/plain") else: object, created = MeetingRegistration.objects.get_or_create( - meeting_id=meeting.pk, - email=email, - reg_type=reg_type) + meeting_id=meeting.pk, email=email, reg_type=reg_type + ) try: # Update attributes - for key in set(data.keys())-set(['attended', 'apikey', 'meeting', 'email']): - if key == 'checkedin': - new = bool(data.get(key).lower() == 'true') + for key in set(data.keys()) - set( + ["attended", "apikey", "meeting", "email"] + ): + if key == "checkedin": + new = bool(data.get(key).lower() == "true") else: new = data.get(key) setattr(object, key, new) @@ -209,15 +269,25 @@ def err(code, text): object.save() except ValueError as e: return err(400, "Unexpected POST data: %s" % e) - response = "Accepted, New registration" if created else "Accepted, Updated registration" - if User.objects.filter(username__iexact=email).exists() or Email.objects.filter(address=email).exists(): + response = ( + "Accepted, New registration" + if created + else "Accepted, Updated registration" + ) + if ( + User.objects.filter(username__iexact=email).exists() + or Email.objects.filter(address=email).exists() + ): pass else: send_account_creation_email(request, email) response += ", Email sent" # handle nomcom volunteer - if request.POST.get('is_nomcom_volunteer', 'false').lower() == 'true' and object.person: + if ( + request.POST.get("is_nomcom_volunteer", "false").lower() == "true" + and object.person + ): try: nomcom = NomCom.objects.get(is_accepting_volunteers=True) except (NomCom.DoesNotExist, NomCom.MultipleObjectsReturned): @@ -228,22 +298,24 @@ def err(code, text): person=object.person, defaults={ "affiliation": data["affiliation"], - "origin": "registration" - } + "origin": "registration", + }, ) - return HttpResponse(response, status=202, content_type='text/plain') + return HttpResponse(response, status=202, content_type="text/plain") else: return HttpResponse(status=405) def version(request): dumpdate = None - dumpinfo = DumpInfo.objects.order_by('-date').first() + dumpinfo = DumpInfo.objects.order_by("-date").first() if dumpinfo: dumpdate = dumpinfo.date if dumpinfo.tz != "UTC": - dumpdate = pytz.timezone(dumpinfo.tz).localize(dumpinfo.date.replace(tzinfo=None)) - dumptime = dumpdate.strftime('%Y-%m-%d %H:%M:%S %z') if dumpinfo else None + dumpdate = pytz.timezone(dumpinfo.tz).localize( + dumpinfo.date.replace(tzinfo=None) + ) + dumptime = dumpdate.strftime("%Y-%m-%d %H:%M:%S %z") if dumpinfo else None # important libraries __version_extra__ = {} @@ -251,39 +323,41 @@ def version(request): __version_extra__[lib] = metadata_version(lib) return HttpResponse( - json.dumps({ - 'version': ietf.__version__+ietf.__patch__, - 'other': __version_extra__, - 'dumptime': dumptime, - }), - content_type='application/json', - ) - + json.dumps( + { + "version": ietf.__version__ + ietf.__patch__, + "other": __version_extra__, + "dumptime": dumptime, + } + ), + content_type="application/json", + ) + @require_api_key @csrf_exempt def app_auth(request, app: Literal["authortools", "bibxml"]): - return HttpResponse( - json.dumps({'success': True}), - content_type='application/json') + return HttpResponse(json.dumps({"success": True}), content_type="application/json") + @requires_api_token @csrf_exempt def nfs_metrics(request): - with NamedTemporaryFile(dir=settings.NFS_METRICS_TMP_DIR,delete=False) as fp: + with NamedTemporaryFile(dir=settings.NFS_METRICS_TMP_DIR, delete=False) as fp: fp.close() mark = datetime.datetime.now() with open(fp.name, mode="w") as f: - f.write("whyioughta"*1024) + f.write("whyioughta" * 1024) write_latency = (datetime.datetime.now() - mark).total_seconds() mark = datetime.datetime.now() with open(fp.name, "r") as f: - _=f.read() + _ = f.read() read_latency = (datetime.datetime.now() - mark).total_seconds() Path(f.name).unlink() - response=f'nfs_latency_seconds{{operation="write"}} {write_latency}\nnfs_latency_seconds{{operation="read"}} {read_latency}\n' + response = f'nfs_latency_seconds{{operation="write"}} {write_latency}\nnfs_latency_seconds{{operation="read"}} {read_latency}\n' return HttpResponse(response) + def find_doc_for_rfcdiff(name, rev): """rfcdiff lookup heuristics @@ -294,22 +368,25 @@ def find_doc_for_rfcdiff(name, rev): [3] - revision actually found (may differ from :rev: input) """ found = fuzzy_find_documents(name, rev) - condition = 'no such document' + condition = "no such document" if found.documents.count() != 1: return (condition, None, None, rev) doc = found.documents.get() if found.matched_rev is None or doc.rev == found.matched_rev: - condition = 'current version' + condition = "current version" return (condition, doc, None, found.matched_rev) else: - candidate = doc.history_set.filter(rev=found.matched_rev).order_by("-time").first() + candidate = ( + doc.history_set.filter(rev=found.matched_rev).order_by("-time").first() + ) if candidate: - condition = 'historic version' + condition = "historic version" return (condition, doc, candidate, found.matched_rev) else: - condition = 'version dochistory not found' + condition = "version dochistory not found" return (condition, doc, None, found.matched_rev) + # This is a proof of concept of a service that would redirect to the current revision # def rfcdiff_latest(request, name, rev=None): # condition, doc, history = find_doc_for_rfcdiff(name, rev) @@ -321,55 +398,506 @@ def find_doc_for_rfcdiff(name, rev): # return redirect(doc.get_href()) HAS_TOMBSTONE = [ - 2821, 2822, 2873, 2919, 2961, 3023, 3029, 3031, 3032, 3033, 3034, 3035, 3036, - 3037, 3038, 3042, 3044, 3050, 3052, 3054, 3055, 3056, 3057, 3059, 3060, 3061, - 3062, 3063, 3064, 3067, 3068, 3069, 3070, 3071, 3072, 3073, 3074, 3075, 3076, - 3077, 3078, 3080, 3081, 3082, 3084, 3085, 3086, 3087, 3088, 3089, 3090, 3094, - 3095, 3096, 3097, 3098, 3101, 3102, 3103, 3104, 3105, 3106, 3107, 3108, 3109, - 3110, 3111, 3112, 3113, 3114, 3115, 3116, 3117, 3118, 3119, 3120, 3121, 3123, - 3124, 3126, 3127, 3128, 3130, 3131, 3132, 3133, 3134, 3135, 3136, 3137, 3138, - 3139, 3140, 3141, 3142, 3143, 3144, 3145, 3147, 3149, 3150, 3151, 3152, 3153, - 3154, 3155, 3156, 3157, 3158, 3159, 3160, 3161, 3162, 3163, 3164, 3165, 3166, - 3167, 3168, 3169, 3170, 3171, 3172, 3173, 3174, 3176, 3179, 3180, 3181, 3182, - 3183, 3184, 3185, 3186, 3187, 3188, 3189, 3190, 3191, 3192, 3193, 3194, 3197, - 3198, 3201, 3202, 3203, 3204, 3205, 3206, 3207, 3208, 3209, 3210, 3211, 3212, - 3213, 3214, 3215, 3216, 3217, 3218, 3220, 3221, 3222, 3224, 3225, 3226, 3227, - 3228, 3229, 3230, 3231, 3232, 3233, 3234, 3235, 3236, 3237, 3238, 3240, 3241, - 3242, 3243, 3244, 3245, 3246, 3247, 3248, 3249, 3250, 3253, 3254, 3255, 3256, - 3257, 3258, 3259, 3260, 3261, 3262, 3263, 3264, 3265, 3266, 3267, 3268, 3269, - 3270, 3271, 3272, 3273, 3274, 3275, 3276, 3278, 3279, 3280, 3281, 3282, 3283, - 3284, 3285, 3286, 3287, 3288, 3289, 3290, 3291, 3292, 3293, 3294, 3295, 3296, - 3297, 3298, 3301, 3302, 3303, 3304, 3305, 3307, 3308, 3309, 3310, 3311, 3312, - 3313, 3315, 3317, 3318, 3319, 3320, 3321, 3322, 3323, 3324, 3325, 3326, 3327, - 3329, 3330, 3331, 3332, 3334, 3335, 3336, 3338, 3340, 3341, 3342, 3343, 3346, - 3348, 3349, 3351, 3352, 3353, 3354, 3355, 3356, 3360, 3361, 3362, 3363, 3364, - 3366, 3367, 3368, 3369, 3370, 3371, 3372, 3374, 3375, 3377, 3378, 3379, 3383, - 3384, 3385, 3386, 3387, 3388, 3389, 3390, 3391, 3394, 3395, 3396, 3397, 3398, - 3401, 3402, 3403, 3404, 3405, 3406, 3407, 3408, 3409, 3410, 3411, 3412, 3413, - 3414, 3415, 3416, 3417, 3418, 3419, 3420, 3421, 3422, 3423, 3424, 3425, 3426, - 3427, 3428, 3429, 3430, 3431, 3433, 3434, 3435, 3436, 3437, 3438, 3439, 3440, - 3441, 3443, 3444, 3445, 3446, 3447, 3448, 3449, 3450, 3451, 3452, 3453, 3454, - 3455, 3458, 3459, 3460, 3461, 3462, 3463, 3464, 3465, 3466, 3467, 3468, 3469, - 3470, 3471, 3472, 3473, 3474, 3475, 3476, 3477, 3480, 3481, 3483, 3485, 3488, - 3494, 3495, 3496, 3497, 3498, 3501, 3502, 3503, 3504, 3505, 3506, 3507, 3508, - 3509, 3511, 3512, 3515, 3516, 3517, 3518, 3520, 3521, 3522, 3523, 3524, 3525, - 3527, 3529, 3530, 3532, 3533, 3534, 3536, 3537, 3538, 3539, 3541, 3543, 3544, - 3545, 3546, 3547, 3548, 3549, 3550, 3551, 3552, 3555, 3556, 3557, 3558, 3559, - 3560, 3562, 3563, 3564, 3565, 3568, 3569, 3570, 3571, 3572, 3573, 3574, 3575, - 3576, 3577, 3578, 3579, 3580, 3581, 3582, 3583, 3584, 3588, 3589, 3590, 3591, - 3592, 3593, 3594, 3595, 3597, 3598, 3601, 3607, 3609, 3610, 3612, 3614, 3615, - 3616, 3625, 3627, 3630, 3635, 3636, 3637, 3638 + 2821, + 2822, + 2873, + 2919, + 2961, + 3023, + 3029, + 3031, + 3032, + 3033, + 3034, + 3035, + 3036, + 3037, + 3038, + 3042, + 3044, + 3050, + 3052, + 3054, + 3055, + 3056, + 3057, + 3059, + 3060, + 3061, + 3062, + 3063, + 3064, + 3067, + 3068, + 3069, + 3070, + 3071, + 3072, + 3073, + 3074, + 3075, + 3076, + 3077, + 3078, + 3080, + 3081, + 3082, + 3084, + 3085, + 3086, + 3087, + 3088, + 3089, + 3090, + 3094, + 3095, + 3096, + 3097, + 3098, + 3101, + 3102, + 3103, + 3104, + 3105, + 3106, + 3107, + 3108, + 3109, + 3110, + 3111, + 3112, + 3113, + 3114, + 3115, + 3116, + 3117, + 3118, + 3119, + 3120, + 3121, + 3123, + 3124, + 3126, + 3127, + 3128, + 3130, + 3131, + 3132, + 3133, + 3134, + 3135, + 3136, + 3137, + 3138, + 3139, + 3140, + 3141, + 3142, + 3143, + 3144, + 3145, + 3147, + 3149, + 3150, + 3151, + 3152, + 3153, + 3154, + 3155, + 3156, + 3157, + 3158, + 3159, + 3160, + 3161, + 3162, + 3163, + 3164, + 3165, + 3166, + 3167, + 3168, + 3169, + 3170, + 3171, + 3172, + 3173, + 3174, + 3176, + 3179, + 3180, + 3181, + 3182, + 3183, + 3184, + 3185, + 3186, + 3187, + 3188, + 3189, + 3190, + 3191, + 3192, + 3193, + 3194, + 3197, + 3198, + 3201, + 3202, + 3203, + 3204, + 3205, + 3206, + 3207, + 3208, + 3209, + 3210, + 3211, + 3212, + 3213, + 3214, + 3215, + 3216, + 3217, + 3218, + 3220, + 3221, + 3222, + 3224, + 3225, + 3226, + 3227, + 3228, + 3229, + 3230, + 3231, + 3232, + 3233, + 3234, + 3235, + 3236, + 3237, + 3238, + 3240, + 3241, + 3242, + 3243, + 3244, + 3245, + 3246, + 3247, + 3248, + 3249, + 3250, + 3253, + 3254, + 3255, + 3256, + 3257, + 3258, + 3259, + 3260, + 3261, + 3262, + 3263, + 3264, + 3265, + 3266, + 3267, + 3268, + 3269, + 3270, + 3271, + 3272, + 3273, + 3274, + 3275, + 3276, + 3278, + 3279, + 3280, + 3281, + 3282, + 3283, + 3284, + 3285, + 3286, + 3287, + 3288, + 3289, + 3290, + 3291, + 3292, + 3293, + 3294, + 3295, + 3296, + 3297, + 3298, + 3301, + 3302, + 3303, + 3304, + 3305, + 3307, + 3308, + 3309, + 3310, + 3311, + 3312, + 3313, + 3315, + 3317, + 3318, + 3319, + 3320, + 3321, + 3322, + 3323, + 3324, + 3325, + 3326, + 3327, + 3329, + 3330, + 3331, + 3332, + 3334, + 3335, + 3336, + 3338, + 3340, + 3341, + 3342, + 3343, + 3346, + 3348, + 3349, + 3351, + 3352, + 3353, + 3354, + 3355, + 3356, + 3360, + 3361, + 3362, + 3363, + 3364, + 3366, + 3367, + 3368, + 3369, + 3370, + 3371, + 3372, + 3374, + 3375, + 3377, + 3378, + 3379, + 3383, + 3384, + 3385, + 3386, + 3387, + 3388, + 3389, + 3390, + 3391, + 3394, + 3395, + 3396, + 3397, + 3398, + 3401, + 3402, + 3403, + 3404, + 3405, + 3406, + 3407, + 3408, + 3409, + 3410, + 3411, + 3412, + 3413, + 3414, + 3415, + 3416, + 3417, + 3418, + 3419, + 3420, + 3421, + 3422, + 3423, + 3424, + 3425, + 3426, + 3427, + 3428, + 3429, + 3430, + 3431, + 3433, + 3434, + 3435, + 3436, + 3437, + 3438, + 3439, + 3440, + 3441, + 3443, + 3444, + 3445, + 3446, + 3447, + 3448, + 3449, + 3450, + 3451, + 3452, + 3453, + 3454, + 3455, + 3458, + 3459, + 3460, + 3461, + 3462, + 3463, + 3464, + 3465, + 3466, + 3467, + 3468, + 3469, + 3470, + 3471, + 3472, + 3473, + 3474, + 3475, + 3476, + 3477, + 3480, + 3481, + 3483, + 3485, + 3488, + 3494, + 3495, + 3496, + 3497, + 3498, + 3501, + 3502, + 3503, + 3504, + 3505, + 3506, + 3507, + 3508, + 3509, + 3511, + 3512, + 3515, + 3516, + 3517, + 3518, + 3520, + 3521, + 3522, + 3523, + 3524, + 3525, + 3527, + 3529, + 3530, + 3532, + 3533, + 3534, + 3536, + 3537, + 3538, + 3539, + 3541, + 3543, + 3544, + 3545, + 3546, + 3547, + 3548, + 3549, + 3550, + 3551, + 3552, + 3555, + 3556, + 3557, + 3558, + 3559, + 3560, + 3562, + 3563, + 3564, + 3565, + 3568, + 3569, + 3570, + 3571, + 3572, + 3573, + 3574, + 3575, + 3576, + 3577, + 3578, + 3579, + 3580, + 3581, + 3582, + 3583, + 3584, + 3588, + 3589, + 3590, + 3591, + 3592, + 3593, + 3594, + 3595, + 3597, + 3598, + 3601, + 3607, + 3609, + 3610, + 3612, + 3614, + 3615, + 3616, + 3625, + 3627, + 3630, + 3635, + 3636, + 3637, + 3638, ] def get_previous_url(name, rev=None): - '''Return previous url''' + """Return previous url""" condition, document, history, found_rev = find_doc_for_rfcdiff(name, rev) - previous_url = '' - if condition in ('historic version', 'current version'): + previous_url = "" + if condition in ("historic version", "current version"): doc = history if history else document previous_url = doc.get_href() - elif condition == 'version dochistory not found': + elif condition == "version dochistory not found": document.rev = found_rev previous_url = document.get_href() return previous_url @@ -380,69 +908,82 @@ def rfcdiff_latest_json(request, name, rev=None): condition, document, history, found_rev = find_doc_for_rfcdiff(name, rev) if document and document.type_id == "rfc": draft = document.came_from_draft() - if condition == 'no such document': + if condition == "no such document": raise Http404 - elif condition in ('historic version', 'current version'): + elif condition in ("historic version", "current version"): doc = history if history else document if doc.type_id == "rfc": - response['content_url'] = doc.get_href() - response['name']=doc.name - if draft: - prev_rev = draft.rev - if doc.rfc_number in HAS_TOMBSTONE and prev_rev != '00': - prev_rev = f'{(int(draft.rev)-1):02d}' - response['previous'] = f'{draft.name}-{prev_rev}' - response['previous_url'] = get_previous_url(draft.name, prev_rev) - elif doc.type_id == "draft" and not found_rev and doc.relateddocument_set.filter(relationship_id="became_rfc").exists(): - rfc = doc.related_that_doc("became_rfc")[0] - response['content_url'] = rfc.get_href() - response['name']=rfc.name - prev_rev = doc.rev - if rfc.rfc_number in HAS_TOMBSTONE and prev_rev != '00': - prev_rev = f'{(int(doc.rev)-1):02d}' - response['previous'] = f'{doc.name}-{prev_rev}' - response['previous_url'] = get_previous_url(doc.name, prev_rev) + response["content_url"] = doc.get_href() + response["name"] = doc.name + if draft: + prev_rev = draft.rev + if doc.rfc_number in HAS_TOMBSTONE and prev_rev != "00": + prev_rev = f"{(int(draft.rev)-1):02d}" + response["previous"] = f"{draft.name}-{prev_rev}" + response["previous_url"] = get_previous_url(draft.name, prev_rev) + elif ( + doc.type_id == "draft" + and not found_rev + and doc.relateddocument_set.filter(relationship_id="became_rfc").exists() + ): + rfc = doc.related_that_doc("became_rfc")[0] + response["content_url"] = rfc.get_href() + response["name"] = rfc.name + prev_rev = doc.rev + if rfc.rfc_number in HAS_TOMBSTONE and prev_rev != "00": + prev_rev = f"{(int(doc.rev)-1):02d}" + response["previous"] = f"{doc.name}-{prev_rev}" + response["previous_url"] = get_previous_url(doc.name, prev_rev) else: - response['content_url'] = doc.get_href() - response['rev'] = doc.rev - response['name'] = doc.name - if doc.rev == '00': - replaces_docs = (history.doc if condition=='historic version' else doc).related_that_doc('replaces') + response["content_url"] = doc.get_href() + response["rev"] = doc.rev + response["name"] = doc.name + if doc.rev == "00": + replaces_docs = ( + history.doc if condition == "historic version" else doc + ).related_that_doc("replaces") if replaces_docs: replaces = replaces_docs[0] - response['previous'] = f'{replaces.name}-{replaces.rev}' - response['previous_url'] = get_previous_url(replaces.name, replaces.rev) + response["previous"] = f"{replaces.name}-{replaces.rev}" + response["previous_url"] = get_previous_url( + replaces.name, replaces.rev + ) else: match = re.search("-(rfc)?([0-9][0-9][0-9]+)bis(-.*)?$", name) if match and match.group(2): - response['previous'] = f'rfc{match.group(2)}' - response['previous_url'] = get_previous_url(f'rfc{match.group(2)}') + response["previous"] = f"rfc{match.group(2)}" + response["previous_url"] = get_previous_url( + f"rfc{match.group(2)}" + ) else: # not sure what to do if non-numeric values come back, so at least log it - log.assertion('doc.rev.isdigit()') - prev_rev = f'{(int(doc.rev)-1):02d}' - response['previous'] = f'{doc.name}-{prev_rev}' - response['previous_url'] = get_previous_url(doc.name, prev_rev) - elif condition == 'version dochistory not found': - response['warning'] = 'History for this version not found - these results are speculation' - response['name'] = document.name - response['rev'] = found_rev + log.assertion("doc.rev.isdigit()") + prev_rev = f"{(int(doc.rev)-1):02d}" + response["previous"] = f"{doc.name}-{prev_rev}" + response["previous_url"] = get_previous_url(doc.name, prev_rev) + elif condition == "version dochistory not found": + response["warning"] = ( + "History for this version not found - these results are speculation" + ) + response["name"] = document.name + response["rev"] = found_rev document.rev = found_rev - response['content_url'] = document.get_href() + response["content_url"] = document.get_href() # not sure what to do if non-numeric values come back, so at least log it - log.assertion('found_rev.isdigit()') + log.assertion("found_rev.isdigit()") if int(found_rev) > 0: - prev_rev = f'{(int(found_rev)-1):02d}' - response['previous'] = f'{document.name}-{prev_rev}' - response['previous_url'] = get_previous_url(document.name, prev_rev) + prev_rev = f"{(int(found_rev)-1):02d}" + response["previous"] = f"{document.name}-{prev_rev}" + response["previous_url"] = get_previous_url(document.name, prev_rev) else: match = re.search("-(rfc)?([0-9][0-9][0-9]+)bis(-.*)?$", name) if match and match.group(2): - response['previous'] = f'rfc{match.group(2)}' - response['previous_url'] = get_previous_url(f'rfc{match.group(2)}') + response["previous"] = f"rfc{match.group(2)}" + response["previous_url"] = get_previous_url(f"rfc{match.group(2)}") if not response: raise Http404 - return HttpResponse(json.dumps(response), content_type='application/json') + return HttpResponse(json.dumps(response), content_type="application/json") + @csrf_exempt def directauth(request): @@ -456,39 +997,60 @@ def directauth(request): if raw_data is None or data is None: log.log("Request body is either missing or invalid") - return HttpResponse(json.dumps(dict(result="failure",reason="invalid post")), content_type='application/json') + return HttpResponse( + json.dumps(dict(result="failure", reason="invalid post")), + content_type="application/json", + ) - authtoken = data.get('authtoken', None) - username = data.get('username', None) - password = data.get('password', None) + authtoken = data.get("authtoken", None) + username = data.get("username", None) + password = data.get("password", None) if any([item is None for item in (authtoken, username, password)]): - log.log("One or more mandatory fields are missing: authtoken, username, password") - return HttpResponse(json.dumps(dict(result="failure",reason="invalid post")), content_type='application/json') + log.log( + "One or more mandatory fields are missing: authtoken, username, password" + ) + return HttpResponse( + json.dumps(dict(result="failure", reason="invalid post")), + content_type="application/json", + ) if not is_valid_token("ietf.api.views.directauth", authtoken): log.log("Auth token provided is invalid") - return HttpResponse(json.dumps(dict(result="failure",reason="invalid authtoken")), content_type='application/json') - + return HttpResponse( + json.dumps(dict(result="failure", reason="invalid authtoken")), + content_type="application/json", + ) + user_query = User.objects.filter(username__iexact=username) # Matching email would be consistent with auth everywhere else in the app, but until we can map users well # in the imap server, people's annotations are associated with a very specific login. # If we get a second user of this API, add an "allow_any_email" argument. - # Note well that we are using user.username, not what was passed to the API. user_count = user_query.count() - if user_count == 1 and authenticate(username = user_query.first().username, password = password): + if user_count == 1 and authenticate( + username=user_query.first().username, password=password + ): user = user_query.get() - if user_query.filter(person__isnull=True).count() == 1: # Can't inspect user.person direclty here - log.log(f"Direct auth success (personless user): {user.pk}:{user.username}") + if ( + user_query.filter(person__isnull=True).count() == 1 + ): # Can't inspect user.person direclty here + log.log( + f"Direct auth success (personless user): {user.pk}:{user.username}" + ) else: log.log(f"Direct auth success: {user.pk}:{user.person.plain_name()}") - return HttpResponse(json.dumps(dict(result="success")), content_type='application/json') + return HttpResponse( + json.dumps(dict(result="success")), content_type="application/json" + ) log.log(f"Direct auth failure: {username} ({user_count} user(s) found)") - return HttpResponse(json.dumps(dict(result="failure", reason="authentication failed")), content_type='application/json') + return HttpResponse( + json.dumps(dict(result="failure", reason="authentication failed")), + content_type="application/json", + ) else: log.log(f"Request must be POST: {request.method} received") @@ -525,7 +1087,7 @@ def group_aliases(request): "alias": alias, "domains": domains, "addresses": address_list, - } + } for alias, domains, address_list in GroupAliasGenerator() ] } @@ -539,7 +1101,9 @@ def active_email_list(request): if request.method == "GET": return JsonResponse( { - "addresses": list(Email.objects.filter(active=True).values_list("address", flat=True)), + "addresses": list( + Email.objects.filter(active=True).values_list("address", flat=True) + ), } ) return HttpResponse(status=405) @@ -578,6 +1142,7 @@ def role_holder_addresses(request): class EmailIngestionError(Exception): """Exception indicating ingestion failed""" + def __init__( self, msg="Message rejected", @@ -585,29 +1150,31 @@ def __init__( email_body: Optional[str] = None, email_recipients: Optional[Iterable[str]] = None, email_attach_traceback=False, - email_original_message: Optional[bytes]=None, + email_original_message: Optional[bytes] = None, ): self.msg = msg self.email_body = email_body self.email_subject = msg - self.email_recipients = email_recipients + self.email_recipients = email_recipients self.email_attach_traceback = email_attach_traceback self.email_original_message = email_original_message self.email_from = settings.SERVER_EMAIL - + @staticmethod def _summarize_error(error): frame = extract_tb(error.__traceback__)[-1] - return dedent(f"""\ + return dedent( + f"""\ Error details: Exception type: {type(error).__module__}.{type(error).__name__} File: {frame.filename} - Line: {frame.lineno}""") + Line: {frame.lineno}""" + ) def as_emailmessage(self) -> Optional[EmailMessage]: """Generate an EmailMessage to report an error""" if self.email_body is None: - return None + return None error = self if self.__cause__ is None else self.__cause__ format_values = dict( error=error, @@ -615,14 +1182,12 @@ def as_emailmessage(self) -> Optional[EmailMessage]: ) msg = EmailMessage() if self.email_recipients is None: - msg["To"] = tuple(adm[1] for adm in settings.ADMINS) - else: + msg["To"] = tuple(adm[1] for adm in settings.ADMINS) + else: msg["To"] = self.email_recipients msg["From"] = self.email_from msg["Subject"] = self.msg - msg.set_content( - self.email_body.format(**format_values) - ) + msg.set_content(self.email_body.format(**format_values)) if self.email_attach_traceback: msg.add_attachment( "".join(format_exception(None, error, error.__traceback__)), @@ -634,19 +1199,20 @@ def as_emailmessage(self) -> Optional[EmailMessage]: # message. msg.add_attachment( self.email_original_message, - 'application', 'octet-stream', # media type - filename='original-message', + "application", + "octet-stream", # media type + filename="original-message", ) return msg def ingest_email_handler(request, test_mode=False): """Ingest incoming email - handler - + Returns a 4xx or 5xx status code if the HTTP request was invalid or something went wrong while processing it. If the request was valid, returns a 200. This may or may not indicate that the message was accepted. - + If test_mode is true, actual processing of a valid message will be skipped. In this mode, a valid request with a valid destination will be treated as accepted. The "bad_dest" error may still be returned. @@ -669,7 +1235,9 @@ def _api_response(result): payload = json.loads(request.body) _response_email_json_validator.validate(payload) except json.decoder.JSONDecodeError as err: - return _http_err(400, f"JSON parse error at line {err.lineno} col {err.colno}: {err.msg}") + return _http_err( + 400, f"JSON parse error at line {err.lineno} col {err.colno}: {err.msg}" + ) except jsonschema.exceptions.ValidationError as err: return _http_err(400, f"JSON schema error at {err.json_path}: {err.message}") except Exception: @@ -692,7 +1260,7 @@ def _api_response(result): if not test_mode: ipr_ingest_response_email(message) elif dest.startswith("nomcom-feedback-"): - maybe_year = dest[len("nomcom-feedback-"):] + maybe_year = dest[len("nomcom-feedback-") :] if maybe_year.isdecimal(): valid_dest = True if not test_mode: @@ -700,7 +1268,9 @@ def _api_response(result): except EmailIngestionError as err: error_email = err.as_emailmessage() if error_email is not None: - with suppress(Exception): # send_smtp logs its own exceptions, ignore them here + with suppress( + Exception + ): # send_smtp logs its own exceptions, ignore them here send_smtp(error_email) return _api_response("bad_msg") @@ -725,7 +1295,7 @@ def ingest_email(request): @csrf_exempt def ingest_email_test(request): """Ingest incoming email test endpoint - + Hands off to ingest_email_handler() with test_mode=True. This allows @requires_api_token to give the test endpoint a distinct token from the real one. """ diff --git a/ietf/bin/aliases-from-json.py b/ietf/bin/aliases-from-json.py index a0c383a1ac..ef0cfac059 100644 --- a/ietf/bin/aliases-from-json.py +++ b/ietf/bin/aliases-from-json.py @@ -27,7 +27,7 @@ def generate_files(records, adest, vdest, postconfirm, vdomain): """Generate files from an iterable of records - + If adest or vdest exists as a file, it will be overwritten. If it is a directory, files with the default names (draft-aliases and draft-virtual) will be created, but existing files _will not_ be overwritten! @@ -49,10 +49,14 @@ def generate_files(records, adest, vdest, postconfirm, vdomain): domains = item["domains"] address_list = item["addresses"] filtername = f"xfilter-{alias}" - afile.write(f'{filtername + ":":64s} "|{postconfirm} filter expand-{alias} {vdomain}"\n') + afile.write( + f'{filtername + ":":64s} "|{postconfirm} filter expand-{alias} {vdomain}"\n' + ) for dom in domains: vfile.write(f"{f'{alias}@{ADOMAINS[dom]}':64s} {filtername}\n") - vfile.write(f"{f'expand-{alias}@{vdomain}':64s} {', '.join(sorted(address_list))}\n") + vfile.write( + f"{f'expand-{alias}@{vdomain}':64s} {', '.join(sorted(address_list))}\n" + ) perms = stat.S_IWUSR | stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH apath.chmod(perms) @@ -68,6 +72,7 @@ def directory_path(val): else: raise argparse.ArgumentTypeError(f"{p} is not a directory") + if __name__ == "__main__": parser = argparse.ArgumentParser( description="Convert a JSON stream of draft alias definitions into alias / virtual alias files." @@ -75,7 +80,7 @@ def directory_path(val): parser.add_argument( "--prefix", required=True, - help="Prefix for output files. Files will be named -aliases and -virtual." + help="Prefix for output files. Files will be named -aliases and -virtual.", ) parser.add_argument( "--output-dir", diff --git a/ietf/celeryapp.py b/ietf/celeryapp.py index fda89c30be..875592cdd0 100644 --- a/ietf/celeryapp.py +++ b/ietf/celeryapp.py @@ -12,15 +12,15 @@ def on_setup_logging(**kwargs): # Set the default Django settings module for the 'celery' program -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ietf.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ietf.settings") -app = celery.Celery('ietf') +app = celery.Celery("ietf") # Using a string here means the worker doesn't have to serialize # the configuration object to child processes. # - namespace='CELERY' means all celery-related configuration keys # should have a `CELERY_` prefix. -app.config_from_object('django.conf:settings', namespace='CELERY') +app.config_from_object("django.conf:settings", namespace="CELERY") # Turn on Scout APM celery instrumentation if configured in the environment scout_key = os.environ.get("DATATRACKER_SCOUT_KEY", None) @@ -42,7 +42,7 @@ def on_setup_logging(**kwargs): # Scout documentation causes failure at startup, likely because Scout # ingests the config greedily before Django is ready. Have not found a # workaround for this other than explicitly configuring Scout. - scout_apm.celery.install() + scout_apm.celery.install() # Load task modules from all registered Django apps. app.autodiscover_tasks() @@ -50,4 +50,4 @@ def on_setup_logging(**kwargs): @app.task(bind=True) def debug_task(self): - print(f'Request: {self.request!r}') + print(f"Request: {self.request!r}") diff --git a/ietf/checks.py b/ietf/checks.py index f911d081f0..c25db7d25b 100644 --- a/ietf/checks.py +++ b/ietf/checks.py @@ -5,9 +5,10 @@ import os import time from textwrap import dedent -from typing import List, Tuple # pyflakes:ignore +from typing import List, Tuple # pyflakes:ignore + +import debug # pyflakes:ignore -import debug # pyflakes:ignore debug.debug = True from django.conf import settings @@ -16,10 +17,12 @@ from django.utils.encoding import force_str import ietf.utils.patch as patch -checks_run = [] # type: List[str] +checks_run = [] # type: List[str] + def already_ran(): import inspect + outerframe = inspect.currentframe().f_back name = outerframe.f_code.co_name if name in checks_run: @@ -28,70 +31,93 @@ def already_ran(): checks_run.append(name) return False -@checks.register('directories') + +@checks.register("directories") def check_id_submission_directories(app_configs, **kwargs): # if already_ran(): return [] # errors = [] - for s in ("IDSUBMIT_STAGING_PATH", "IDSUBMIT_REPOSITORY_PATH", "INTERNET_DRAFT_ARCHIVE_DIR", ): + for s in ( + "IDSUBMIT_STAGING_PATH", + "IDSUBMIT_REPOSITORY_PATH", + "INTERNET_DRAFT_ARCHIVE_DIR", + ): p = getattr(settings, s) if not os.path.exists(p): - errors.append(checks.Critical( - "A directory used by the I-D submission tool does not\n" - "exist at the path given in the settings file. The setting is:\n" - " %s = %s" % (s, p), - hint = ("Please either update the local settings to point at the correct\n" - "\tdirectory, or if the setting is correct, create the indicated directory.\n"), - id = "datatracker.E0006", - )) + errors.append( + checks.Critical( + "A directory used by the I-D submission tool does not\n" + "exist at the path given in the settings file. The setting is:\n" + " %s = %s" % (s, p), + hint=( + "Please either update the local settings to point at the correct\n" + "\tdirectory, or if the setting is correct, create the indicated directory.\n" + ), + id="datatracker.E0006", + ) + ) return errors -@checks.register('files') + +@checks.register("files") def check_id_submission_files(app_configs, **kwargs): # if already_ran(): return [] # errors = [] - for s in ("IDSUBMIT_IDNITS_BINARY", ): + for s in ("IDSUBMIT_IDNITS_BINARY",): p = getattr(settings, s) if not os.path.exists(p): - errors.append(checks.Critical( - "A file used by the I-D submission tool does not exist\n" - "at the path given in the settings file. The setting is:\n" - " %s = %s" % (s, p), - hint = ("Please either update the local settings to point at the correct\n" - "\tfile, or if the setting is correct, make sure the file is in place and\n" - "\thas the right permissions.\n"), - id = "datatracker.E0007", - )) + errors.append( + checks.Critical( + "A file used by the I-D submission tool does not exist\n" + "at the path given in the settings file. The setting is:\n" + " %s = %s" % (s, p), + hint=( + "Please either update the local settings to point at the correct\n" + "\tfile, or if the setting is correct, make sure the file is in place and\n" + "\thas the right permissions.\n" + ), + id="datatracker.E0007", + ) + ) return errors -@checks.register('directories') +@checks.register("directories") def check_yang_model_directories(app_configs, **kwargs): # if already_ran(): return [] # errors = [] - for s in ("SUBMIT_YANG_RFC_MODEL_DIR", "SUBMIT_YANG_DRAFT_MODEL_DIR", "SUBMIT_YANG_IANA_MODEL_DIR", "SUBMIT_YANG_CATALOG_MODEL_DIR",): + for s in ( + "SUBMIT_YANG_RFC_MODEL_DIR", + "SUBMIT_YANG_DRAFT_MODEL_DIR", + "SUBMIT_YANG_IANA_MODEL_DIR", + "SUBMIT_YANG_CATALOG_MODEL_DIR", + ): p = getattr(settings, s) if not os.path.exists(p): - errors.append(checks.Critical( - "A directory used by the yang validation tools does\n" - "not exist at the path gvien in the settings file. The setting is:\n" - " %s = %s" % (s, p), - hint = ("Please either update your local settings to point at the correct\n" - "\tdirectory, or if the setting is correct, create the indicated directory.\n"), - id = "datatracker.E0017", - )) + errors.append( + checks.Critical( + "A directory used by the yang validation tools does\n" + "not exist at the path gvien in the settings file. The setting is:\n" + " %s = %s" % (s, p), + hint=( + "Please either update your local settings to point at the correct\n" + "\tdirectory, or if the setting is correct, create the indicated directory.\n" + ), + id="datatracker.E0017", + ) + ) return errors -@checks.register('submission-checkers') +@checks.register("submission-checkers") def check_id_submission_checkers(app_configs, **kwargs): # if already_ran(): @@ -102,129 +128,162 @@ def check_id_submission_checkers(app_configs, **kwargs): try: checker_class = import_string(checker_path) except Exception as e: - errors.append(checks.Critical( - "An exception was raised when trying to import the\n" - "Internet-Draft submission checker class '%s':\n %s" % (checker_path, e), - hint = "Please check that the class exists and can be imported.\n", - id = "datatracker.E0008", - )) + errors.append( + checks.Critical( + "An exception was raised when trying to import the\n" + "Internet-Draft submission checker class '%s':\n %s" + % (checker_path, e), + hint="Please check that the class exists and can be imported.\n", + id="datatracker.E0008", + ) + ) try: checker = checker_class() except Exception as e: - errors.append(checks.Critical( - "An exception was raised when trying to instantiate\n" - "the Internet-Draft submission checker class '%s':\n %s" % (checker_path, e), - hint = "Please check that the class can be instantiated.\n", - id = "datatracker.E0009", - )) + errors.append( + checks.Critical( + "An exception was raised when trying to instantiate\n" + "the Internet-Draft submission checker class '%s':\n %s" + % (checker_path, e), + hint="Please check that the class can be instantiated.\n", + id="datatracker.E0009", + ) + ) continue - for attr in ('name',): + for attr in ("name",): if not hasattr(checker, attr): - errors.append(checks.Critical( - "The Internet-Draft submission checker\n '%s'\n" - "has no attribute '%s', which is required" % (checker_path, attr), - hint = "Please update the class.\n", - id = "datatracker.E0010", - )) - checker_methods = ("check_file_txt", "check_file_xml", "check_fragment_txt", "check_fragment_xml", ) + errors.append( + checks.Critical( + "The Internet-Draft submission checker\n '%s'\n" + "has no attribute '%s', which is required" + % (checker_path, attr), + hint="Please update the class.\n", + id="datatracker.E0010", + ) + ) + checker_methods = ( + "check_file_txt", + "check_file_xml", + "check_fragment_txt", + "check_fragment_xml", + ) for method in checker_methods: if hasattr(checker, method): break else: - errors.append(checks.Critical( - "The Internet-Draft submission checker\n '%s'\n" - " has no recognised checker method; " - "should be one or more of %s." % (checker_path, checker_methods), - hint = "Please update the class.\n", - id = "datatracker.E0011", - )) + errors.append( + checks.Critical( + "The Internet-Draft submission checker\n '%s'\n" + " has no recognised checker method; " + "should be one or more of %s." % (checker_path, checker_methods), + hint="Please update the class.\n", + id="datatracker.E0011", + ) + ) return errors -@checks.register('directories') + +@checks.register("directories") def check_media_directories(app_configs, **kwargs): # if already_ran(): return [] # errors = [] - for s in ("PHOTOS_DIR", ): + for s in ("PHOTOS_DIR",): p = getattr(settings, s) if not os.path.exists(p): - errors.append(checks.Critical( - "A directory used for media uploads and serves does\n" - "not exist at the path given in the settings file. The setting is:\n" - " %s = %s" % (s, p), - hint = ("Please either update the local settings to point at the correct\n" - "\tdirectory, or if the setting is correct, create the indicated directory.\n"), - id = "datatracker.E0012", - )) + errors.append( + checks.Critical( + "A directory used for media uploads and serves does\n" + "not exist at the path given in the settings file. The setting is:\n" + " %s = %s" % (s, p), + hint=( + "Please either update the local settings to point at the correct\n" + "\tdirectory, or if the setting is correct, create the indicated directory.\n" + ), + id="datatracker.E0012", + ) + ) return errors - -@checks.register('directories') + +@checks.register("directories") def check_proceedings_directories(app_configs, **kwargs): # if already_ran(): return [] # errors = [] - for s in ("AGENDA_PATH", ): + for s in ("AGENDA_PATH",): p = getattr(settings, s) if not os.path.exists(p): - errors.append(checks.Critical( - "A directory used for meeting materials does not\n" - "exist at the path given in the settings file. The setting is:\n" - " %s = %s" % (s, p), - hint = ("Please either update the local settings to point at the correct\n" - "\tdirectory, or if the setting is correct, create the indicated directory.\n"), - id = "datatracker.E0013", - )) + errors.append( + checks.Critical( + "A directory used for meeting materials does not\n" + "exist at the path given in the settings file. The setting is:\n" + " %s = %s" % (s, p), + hint=( + "Please either update the local settings to point at the correct\n" + "\tdirectory, or if the setting is correct, create the indicated directory.\n" + ), + id="datatracker.E0013", + ) + ) return errors -@checks.register('cache') + +@checks.register("cache") def check_cache(app_configs, **kwargs): # if already_ran(): return [] # errors = [] - if settings.SERVER_MODE == 'production': + if settings.SERVER_MODE == "production": from django.core.cache import cache + def cache_error(msg, errnum): return checks.Warning( - ( "A cache test failed with the message:\n '%s'.\n" - "This indicates that the cache is unavailable or not working as expected.\n" - "It will impact performance, but isn't fatal. The default cache is:\n" - " CACHES['default']['BACKEND'] = %s") % ( + ( + "A cache test failed with the message:\n '%s'.\n" + "This indicates that the cache is unavailable or not working as expected.\n" + "It will impact performance, but isn't fatal. The default cache is:\n" + " CACHES['default']['BACKEND'] = %s" + ) + % ( msg, settings.CACHES["default"]["BACKEND"], ), - hint = "Please check that the configured cache backend is available.\n", - id = "datatracker.%s" % errnum, + hint="Please check that the configured cache backend is available.\n", + id="datatracker.%s" % errnum, ) + cache_key = "checks:check_cache" val = os.urandom(32) wait = 1 cache.set(cache_key, val, wait) if not cache.get(cache_key) == val: errors.append(cache_error("Could not get value from cache", "E0014")) - time.sleep(wait+1) + time.sleep(wait + 1) # should have timed out if cache.get(cache_key) == val: errors.append(cache_error("Cache value didn't time out", "E0015")) cache.set(cache_key, val, settings.SESSION_COOKIE_AGE) if not cache.get(cache_key) == val: - errors.append(cache_error("Cache didn't accept session cookie age", "E0016")) + errors.append( + cache_error("Cache didn't accept session cookie age", "E0016") + ) return errors - -@checks.register('files') +@checks.register("files") def maybe_patch_library(app_configs, **kwargs): errors = [] # Change path to our copy of django (this assumes we're running in a # virtualenv, which we should) import os, django, sys + django_path = os.path.dirname(django.__file__) library_path = os.path.dirname(django_path) top_dir = os.path.dirname(settings.BASE_DIR) @@ -236,34 +295,51 @@ def maybe_patch_library(app_configs, **kwargs): patch_path = os.path.join(top_dir, patch_file) patch_set = patch.fromfile(patch_path) if patch_set: - if not patch_set.apply(root=library_path.encode('utf-8')): - errors.append(checks.Warning( - "Could not apply patch from file '%s'"%patch_file, - hint=("Make sure that the patch file contains a unified diff and has valid file paths\n\n" - "\tPatch root: %s\n" - "\tTarget files: %s\n") % (library_path, ', '.join(force_str(i.target) for i in patch_set.items)), - id="datatracker.W0002", - )) + if not patch_set.apply(root=library_path.encode("utf-8")): + errors.append( + checks.Warning( + "Could not apply patch from file '%s'" % patch_file, + hint=( + "Make sure that the patch file contains a unified diff and has valid file paths\n\n" + "\tPatch root: %s\n" + "\tTarget files: %s\n" + ) + % ( + library_path, + ", ".join( + force_str(i.target) for i in patch_set.items + ), + ), + id="datatracker.W0002", + ) + ) else: # Patch succeeded or was a no-op - if (not patch_set.already_patched - and settings.SERVER_MODE != 'production' - and sys.argv[1] != 'check'): + if ( + not patch_set.already_patched + and settings.SERVER_MODE != "production" + and sys.argv[1] != "check" + ): errors.append( - checks.Error("Found an unpatched file, and applied the patch in %s" % (patch_file), + checks.Error( + "Found an unpatched file, and applied the patch in %s" + % (patch_file), hint="You will need to re-run the command now that the patch in place.", id="datatracker.E0022", ) ) else: - errors.append(checks.Warning( - "Could not parse patch file '%s'"%patch_file, - hint="Make sure that the patch file contains a unified diff", - id="datatracker.W0001", - )) + errors.append( + checks.Warning( + "Could not parse patch file '%s'" % patch_file, + hint="Make sure that the patch file contains a unified diff", + id="datatracker.W0001", + ) + ) except IOError as e: errors.append( - checks.Warning("Could not apply patch from %s: %s" % (patch_file, e), + checks.Warning( + "Could not apply patch from %s: %s" % (patch_file, e), hint="Check file permissions and locations", id="datatracker.W0003", ) @@ -271,16 +347,22 @@ def maybe_patch_library(app_configs, **kwargs): pass return errors -@checks.register('security') + +@checks.register("security") def check_api_key_in_local_settings(app_configs, **kwargs): errors = [] import ietf.settings_local - if settings.SERVER_MODE == 'production': - if not ( hasattr(ietf.settings_local, 'API_PUBLIC_KEY_PEM') - and hasattr(ietf.settings_local, 'API_PRIVATE_KEY_PEM')): - errors.append(checks.Critical( - "There are no API key settings in your settings_local.py", - hint = dedent(""" + + if settings.SERVER_MODE == "production": + if not ( + hasattr(ietf.settings_local, "API_PUBLIC_KEY_PEM") + and hasattr(ietf.settings_local, "API_PRIVATE_KEY_PEM") + ): + errors.append( + checks.Critical( + "There are no API key settings in your settings_local.py", + hint=dedent( + """ You are running in production mode, and need API key settings that are different than the default settings. Please add settings for API_PUBLIC_KEY_PEM and API_PRIVATE_KEY_PEM to your settings local. The @@ -288,22 +370,33 @@ def check_api_key_in_local_settings(app_configs, **kwargs): can generate a suitable keypair with 'ssh-keygen -f apikey.pem', and then extract the public key with 'openssl rsa -in apikey.pem -pubout > apikey.pub'. - """).replace('\n', '\n ').rstrip(), - id = "datatracker.E0020", - )) - elif not ( ietf.settings_local.API_PUBLIC_KEY_PEM == settings.API_PUBLIC_KEY_PEM - and ietf.settings_local.API_PRIVATE_KEY_PEM == settings.API_PRIVATE_KEY_PEM ): - errors.append(checks.Critical( - "Your API key settings in your settings_local.py are not picked up in settings.", - hint = dedent(""" + """ + ) + .replace("\n", "\n ") + .rstrip(), + id="datatracker.E0020", + ) + ) + elif not ( + ietf.settings_local.API_PUBLIC_KEY_PEM == settings.API_PUBLIC_KEY_PEM + and ietf.settings_local.API_PRIVATE_KEY_PEM == settings.API_PRIVATE_KEY_PEM + ): + errors.append( + checks.Critical( + "Your API key settings in your settings_local.py are not picked up in settings.", + hint=dedent( + """ You are running in production mode, and need API key settings which are different than the default settings. You seem to have API key settings in settings_local.py, but they don't seem to propagate to django.conf.settings. Please check if you have multiple settings_local.py files. - """).replace('\n', '\n ').rstrip(), - id = "datatracker.E0021", - )) + """ + ) + .replace("\n", "\n ") + .rstrip(), + id="datatracker.E0021", + ) + ) return errors - diff --git a/ietf/community/admin.py b/ietf/community/admin.py index 4c947ad3f7..7b8a428ed9 100644 --- a/ietf/community/admin.py +++ b/ietf/community/admin.py @@ -6,19 +6,45 @@ from ietf.community.models import CommunityList, SearchRule, EmailSubscription + class CommunityListAdmin(admin.ModelAdmin): - list_display = ['id', 'person', 'group'] - raw_id_fields = ['person', 'group', 'added_docs'] + list_display = ["id", "person", "group"] + raw_id_fields = ["person", "group", "added_docs"] + + admin.site.register(CommunityList, CommunityListAdmin) + class SearchRuleAdmin(admin.ModelAdmin): - list_display = ['id', 'community_list', 'rule_type', 'state', 'group', 'person', 'text'] - raw_id_fields = ['community_list', 'state', 'group', 'person', 'name_contains_index'] - search_fields = ['person__name', 'group__acronym', 'text', ] + list_display = [ + "id", + "community_list", + "rule_type", + "state", + "group", + "person", + "text", + ] + raw_id_fields = [ + "community_list", + "state", + "group", + "person", + "name_contains_index", + ] + search_fields = [ + "person__name", + "group__acronym", + "text", + ] + + admin.site.register(SearchRule, SearchRuleAdmin) + class EmailSubscriptionAdmin(admin.ModelAdmin): - list_display = ['id', 'community_list', 'email', 'notify_on'] - raw_id_fields = ['community_list', 'email'] -admin.site.register(EmailSubscription, EmailSubscriptionAdmin) + list_display = ["id", "community_list", "email", "notify_on"] + raw_id_fields = ["community_list", "email"] + +admin.site.register(EmailSubscription, EmailSubscriptionAdmin) diff --git a/ietf/community/forms.py b/ietf/community/forms.py index d3fa01dd19..937cc7197f 100644 --- a/ietf/community/forms.py +++ b/ietf/community/forms.py @@ -12,27 +12,36 @@ from ietf.person.models import Person from ietf.person.fields import SearchablePersonField + class AddDocumentsForm(forms.Form): - documents = SearchableDocumentsField(label="Add Internet-Drafts to track", doc_type="draft") + documents = SearchableDocumentsField( + label="Add Internet-Drafts to track", doc_type="draft" + ) + class SearchRuleTypeForm(forms.Form): - rule_type = forms.ChoiceField(choices=[('', '--------------')] + SearchRule.RULE_TYPES) + rule_type = forms.ChoiceField( + choices=[("", "--------------")] + SearchRule.RULE_TYPES + ) + class SearchRuleForm(forms.ModelForm): person = SearchablePersonField() class Meta: model = SearchRule - fields = ('state', 'group', 'person', 'text') + fields = ("state", "group", "person", "text") def __init__(self, clist, rule_type, *args, **kwargs): - kwargs["prefix"] = rule_type # add prefix to avoid mixups in the Javascript + kwargs["prefix"] = rule_type # add prefix to avoid mixups in the Javascript super(SearchRuleForm, self).__init__(*args, **kwargs) def restrict_state(state_type, slug=None): if "state" not in self.fields: - raise RuntimeError(f"Rule type {rule_type} cannot include state filtering") - f = self.fields['state'] + raise RuntimeError( + f"Rule type {rule_type} cannot include state filtering" + ) + f = self.fields["state"] f.queryset = f.queryset.filter(used=True).filter(type=state_type) if slug: f.queryset = f.queryset.filter(slug=slug) @@ -49,12 +58,20 @@ def restrict_state(state_type, slug=None): else: if not rule_type.endswith("_rfc"): restrict_state("draft", "active") - + if rule_type.startswith("area"): self.fields["group"].label = "Area" - self.fields["group"].queryset = self.fields["group"].queryset.filter(Q(type="area") | Q(acronym="irtf")).order_by("acronym") + self.fields["group"].queryset = ( + self.fields["group"] + .queryset.filter(Q(type="area") | Q(acronym="irtf")) + .order_by("acronym") + ) else: - self.fields["group"].queryset = self.fields["group"].queryset.filter(type__in=("wg", "rg", "ag", "rag", "program" )).order_by("acronym") + self.fields["group"].queryset = ( + self.fields["group"] + .queryset.filter(type__in=("wg", "rg", "ag", "rag", "program")) + .order_by("acronym") + ) del self.fields["person"] del self.fields["text"] @@ -85,7 +102,13 @@ def restrict_state(state_type, slug=None): self.fields["person"].label = "Shepherd" elif rule_type.startswith("ad"): self.fields["person"].label = "Area Director" - self.fields["person"] = forms.ModelChoiceField(queryset=Person.objects.filter(role__name__in=("ad", "pre-ad"), role__group__state="active").distinct().order_by("name")) + self.fields["person"] = forms.ModelChoiceField( + queryset=Person.objects.filter( + role__name__in=("ad", "pre-ad"), role__group__state="active" + ) + .distinct() + .order_by("name") + ) del self.fields["group"] del self.fields["text"] @@ -97,15 +120,22 @@ def restrict_state(state_type, slug=None): del self.fields["person"] del self.fields["group"] - if 'group' in self.fields: - self.fields['group'].queryset = self.fields['group'].queryset.filter(state="active").order_by("acronym") - self.fields['group'].choices = [(g.pk, "%s - %s" % (g.acronym, g.name)) for g in self.fields['group'].queryset] + if "group" in self.fields: + self.fields["group"].queryset = ( + self.fields["group"].queryset.filter(state="active").order_by("acronym") + ) + self.fields["group"].choices = [ + (g.pk, "%s - %s" % (g.acronym, g.name)) + for g in self.fields["group"].queryset + ] for name, f in self.fields.items(): f.required = True def clean_text(self): - candidate_text = self.cleaned_data["text"].strip().lower() # names are always lower case + candidate_text = ( + self.cleaned_data["text"].strip().lower() + ) # names are always lower case try: re.compile(candidate_text) except re.error as e: @@ -119,19 +149,33 @@ def __init__(self, person, clist, *args, **kwargs): super(SubscriptionForm, self).__init__(*args, **kwargs) - self.fields["notify_on"].widget = forms.RadioSelect(choices=self.fields["notify_on"].choices) - self.fields["email"].queryset = self.fields["email"].queryset.filter(person=person, active=True).order_by("-primary") - self.fields["email"].widget = forms.RadioSelect(choices=[t for t in self.fields["email"].choices if t[0]]) + self.fields["notify_on"].widget = forms.RadioSelect( + choices=self.fields["notify_on"].choices + ) + self.fields["email"].queryset = ( + self.fields["email"] + .queryset.filter(person=person, active=True) + .order_by("-primary") + ) + self.fields["email"].widget = forms.RadioSelect( + choices=[t for t in self.fields["email"].choices if t[0]] + ) if self.fields["email"].queryset: self.fields["email"].initial = self.fields["email"].queryset[0] def clean_email(self): - self.cleaned_data["email"].address = self.cleaned_data["email"].address.strip().lower() + self.cleaned_data["email"].address = ( + self.cleaned_data["email"].address.strip().lower() + ) return self.cleaned_data["email"] def clean(self): - if EmailSubscription.objects.filter(community_list=self.clist, email=self.cleaned_data["email"], notify_on=self.cleaned_data["notify_on"]).exists(): + if EmailSubscription.objects.filter( + community_list=self.clist, + email=self.cleaned_data["email"], + notify_on=self.cleaned_data["notify_on"], + ).exists(): raise forms.ValidationError("You already have a subscription like this.") class Meta: diff --git a/ietf/community/migrations/0001_initial.py b/ietf/community/migrations/0001_initial.py index 44154687f3..d8f9ca3366 100644 --- a/ietf/community/migrations/0001_initial.py +++ b/ietf/community/migrations/0001_initial.py @@ -10,30 +10,137 @@ class Migration(migrations.Migration): initial = True - dependencies: List[Tuple[str, str]] = [ - ] + dependencies: List[Tuple[str, str]] = [] operations = [ migrations.CreateModel( - name='CommunityList', + name="CommunityList", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), ], ), migrations.CreateModel( - name='EmailSubscription', + name="EmailSubscription", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('notify_on', models.CharField(choices=[('all', 'All changes'), ('significant', 'Only significant state changes')], default='all', max_length=30)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "notify_on", + models.CharField( + choices=[ + ("all", "All changes"), + ("significant", "Only significant state changes"), + ], + default="all", + max_length=30, + ), + ), ], ), migrations.CreateModel( - name='SearchRule', + name="SearchRule", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('rule_type', models.CharField(choices=[('group', 'All I-Ds associated with a particular group'), ('area', 'All I-Ds associated with all groups in a particular Area'), ('group_rfc', 'All RFCs associated with a particular group'), ('area_rfc', 'All RFCs associated with all groups in a particular Area'), ('group_exp', 'All expired I-Ds of a particular group'), ('state_iab', 'All I-Ds that are in a particular IAB state'), ('state_iana', 'All I-Ds that are in a particular IANA state'), ('state_iesg', 'All I-Ds that are in a particular IESG state'), ('state_irtf', 'All I-Ds that are in a particular IRTF state'), ('state_ise', 'All I-Ds that are in a particular ISE state'), ('state_rfceditor', 'All I-Ds that are in a particular RFC Editor state'), ('state_ietf', 'All I-Ds that are in a particular Working Group state'), ('author', 'All I-Ds with a particular author'), ('author_rfc', 'All RFCs with a particular author'), ('ad', 'All I-Ds with a particular responsible AD'), ('shepherd', 'All I-Ds with a particular document shepherd'), ('name_contains', 'All I-Ds with particular text/regular expression in the name')], max_length=30)), - ('text', models.CharField(blank=True, default='', max_length=255, verbose_name='Text/RegExp')), - ('community_list', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='community.CommunityList')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "rule_type", + models.CharField( + choices=[ + ("group", "All I-Ds associated with a particular group"), + ( + "area", + "All I-Ds associated with all groups in a particular Area", + ), + ( + "group_rfc", + "All RFCs associated with a particular group", + ), + ( + "area_rfc", + "All RFCs associated with all groups in a particular Area", + ), + ("group_exp", "All expired I-Ds of a particular group"), + ( + "state_iab", + "All I-Ds that are in a particular IAB state", + ), + ( + "state_iana", + "All I-Ds that are in a particular IANA state", + ), + ( + "state_iesg", + "All I-Ds that are in a particular IESG state", + ), + ( + "state_irtf", + "All I-Ds that are in a particular IRTF state", + ), + ( + "state_ise", + "All I-Ds that are in a particular ISE state", + ), + ( + "state_rfceditor", + "All I-Ds that are in a particular RFC Editor state", + ), + ( + "state_ietf", + "All I-Ds that are in a particular Working Group state", + ), + ("author", "All I-Ds with a particular author"), + ("author_rfc", "All RFCs with a particular author"), + ("ad", "All I-Ds with a particular responsible AD"), + ( + "shepherd", + "All I-Ds with a particular document shepherd", + ), + ( + "name_contains", + "All I-Ds with particular text/regular expression in the name", + ), + ], + max_length=30, + ), + ), + ( + "text", + models.CharField( + blank=True, + default="", + max_length=255, + verbose_name="Text/RegExp", + ), + ), + ( + "community_list", + ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="community.CommunityList", + ), + ), ], ), ] diff --git a/ietf/community/migrations/0002_auto_20230320_1222.py b/ietf/community/migrations/0002_auto_20230320_1222.py index f552cc06e1..9503efd481 100644 --- a/ietf/community/migrations/0002_auto_20230320_1222.py +++ b/ietf/community/migrations/0002_auto_20230320_1222.py @@ -11,57 +11,87 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('person', '0001_initial'), + ("person", "0001_initial"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('community', '0001_initial'), - ('group', '0001_initial'), - ('doc', '0001_initial'), + ("community", "0001_initial"), + ("group", "0001_initial"), + ("doc", "0001_initial"), ] operations = [ migrations.AddField( - model_name='searchrule', - name='group', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='group.Group'), + model_name="searchrule", + name="group", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="group.Group", + ), ), migrations.AddField( - model_name='searchrule', - name='name_contains_index', - field=models.ManyToManyField(to='doc.Document'), + model_name="searchrule", + name="name_contains_index", + field=models.ManyToManyField(to="doc.Document"), ), migrations.AddField( - model_name='searchrule', - name='person', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='person.Person'), + model_name="searchrule", + name="person", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="person.Person", + ), ), migrations.AddField( - model_name='searchrule', - name='state', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='doc.State'), + model_name="searchrule", + name="state", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="doc.State", + ), ), migrations.AddField( - model_name='emailsubscription', - name='community_list', - field=ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='community.CommunityList'), + model_name="emailsubscription", + name="community_list", + field=ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="community.CommunityList", + ), ), migrations.AddField( - model_name='emailsubscription', - name='email', - field=ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='person.Email'), + model_name="emailsubscription", + name="email", + field=ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="person.Email" + ), ), migrations.AddField( - model_name='communitylist', - name='added_docs', - field=models.ManyToManyField(to='doc.Document'), + model_name="communitylist", + name="added_docs", + field=models.ManyToManyField(to="doc.Document"), ), migrations.AddField( - model_name='communitylist', - name='group', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='group.Group'), + model_name="communitylist", + name="group", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="group.Group", + ), ), migrations.AddField( - model_name='communitylist', - name='user', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + model_name="communitylist", + name="user", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), ), ] diff --git a/ietf/community/migrations/0003_track_rfcs.py b/ietf/community/migrations/0003_track_rfcs.py index 3c2d04097d..08da783f18 100644 --- a/ietf/community/migrations/0003_track_rfcs.py +++ b/ietf/community/migrations/0003_track_rfcs.py @@ -28,6 +28,7 @@ def forward(apps, schema_editor): rfc_rules = SearchRule.objects.filter(rule_type__endswith="_rfc") rfc_rules.update(state=None) + def reverse(apps, schema_editor): Document = apps.get_model("doc", "Document") for rfc in Document.objects.filter(type__slug="rfc"): diff --git a/ietf/community/migrations/0004_delete_useless_community_lists.py b/ietf/community/migrations/0004_delete_useless_community_lists.py index 9f657a3c34..9573b33bf5 100644 --- a/ietf/community/migrations/0004_delete_useless_community_lists.py +++ b/ietf/community/migrations/0004_delete_useless_community_lists.py @@ -8,12 +8,10 @@ def forward(apps, schema_editor): # As of 2024-01-05, there are 570 personal CommunityLists with a user # who has no associated Person. None of these has an EmailSubscription, # so the lists are doing nothing and can be safely deleted. - personal_lists_no_person = CommunityList.objects.exclude( - user__isnull=True - ).filter( + personal_lists_no_person = CommunityList.objects.exclude(user__isnull=True).filter( user__person__isnull=True ) - # Confirm the assumption that none of the lists to be deleted has an EmailSubscription + # Confirm the assumption that none of the lists to be deleted has an EmailSubscription assert not personal_lists_no_person.filter(emailsubscription__isnull=False).exists() personal_lists_no_person.delete() diff --git a/ietf/community/migrations/0005_user_to_person.py b/ietf/community/migrations/0005_user_to_person.py index 01d8950edb..dc68905559 100644 --- a/ietf/community/migrations/0005_user_to_person.py +++ b/ietf/community/migrations/0005_user_to_person.py @@ -7,7 +7,7 @@ def forward(apps, schema_editor): - CommunityList = apps.get_model('community', 'CommunityList') + CommunityList = apps.get_model("community", "CommunityList") for clist in CommunityList.objects.all(): try: clist.person = clist.user.person @@ -15,8 +15,9 @@ def forward(apps, schema_editor): clist.person = None clist.save() + def reverse(apps, schema_editor): - CommunityList = apps.get_model('community', 'CommunityList') + CommunityList = apps.get_model("community", "CommunityList") for clist in CommunityList.objects.all(): try: clist.user = clist.person.user @@ -24,6 +25,7 @@ def reverse(apps, schema_editor): clist.user = None clist.save() + class Migration(migrations.Migration): dependencies = [ ("community", "0004_delete_useless_community_lists"), @@ -49,6 +51,7 @@ class Migration(migrations.Migration): blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL), + to=settings.AUTH_USER_MODEL, + ), ), ] diff --git a/ietf/community/models.py b/ietf/community/models.py index 6945918f9a..07d2dd207b 100644 --- a/ietf/community/models.py +++ b/ietf/community/models.py @@ -17,21 +17,28 @@ class CommunityList(models.Model): def long_name(self): if self.person: - return 'Personal I-D list of %s' % self.person.plain_name() + return "Personal I-D list of %s" % self.person.plain_name() elif self.group: - return 'I-D list for %s' % self.group.name + return "I-D list for %s" % self.group.name else: - return 'I-D list' + return "I-D list" def __str__(self): return self.long_name() def get_absolute_url(self): import ietf.community.views + if self.person: - return urlreverse(ietf.community.views.view_list, kwargs={ 'email_or_name': self.person.email() }) + return urlreverse( + ietf.community.views.view_list, + kwargs={"email_or_name": self.person.email()}, + ) elif self.group: - return urlreverse("ietf.group.views.group_documents", kwargs={ 'acronym': self.group.acronym }) + return urlreverse( + "ietf.group.views.group_documents", + kwargs={"acronym": self.group.acronym}, + ) return "" @@ -39,28 +46,26 @@ class SearchRule(models.Model): # these types define the UI for setting up the rule, and also # helps when interpreting the rule and matching documents RULE_TYPES = [ - ('group', 'All I-Ds associated with a particular group'), - ('area', 'All I-Ds associated with all groups in a particular Area'), - ('group_rfc', 'All RFCs associated with a particular group'), - ('area_rfc', 'All RFCs associated with all groups in a particular Area'), - ('group_exp', 'All expired I-Ds of a particular group'), - - ('state_iab', 'All I-Ds that are in a particular IAB state'), - ('state_iana', 'All I-Ds that are in a particular IANA state'), - ('state_iesg', 'All I-Ds that are in a particular IESG state'), - ('state_irtf', 'All I-Ds that are in a particular IRTF state'), - ('state_ise', 'All I-Ds that are in a particular ISE state'), - ('state_rfceditor', 'All I-Ds that are in a particular RFC Editor state'), - ('state_ietf', 'All I-Ds that are in a particular Working Group state'), - - ('author', 'All I-Ds with a particular author'), - ('author_rfc', 'All RFCs with a particular author'), - - ('ad', 'All I-Ds with a particular responsible AD'), - - ('shepherd', 'All I-Ds with a particular document shepherd'), - - ('name_contains', 'All I-Ds with particular text/regular expression in the name'), + ("group", "All I-Ds associated with a particular group"), + ("area", "All I-Ds associated with all groups in a particular Area"), + ("group_rfc", "All RFCs associated with a particular group"), + ("area_rfc", "All RFCs associated with all groups in a particular Area"), + ("group_exp", "All expired I-Ds of a particular group"), + ("state_iab", "All I-Ds that are in a particular IAB state"), + ("state_iana", "All I-Ds that are in a particular IANA state"), + ("state_iesg", "All I-Ds that are in a particular IESG state"), + ("state_irtf", "All I-Ds that are in a particular IRTF state"), + ("state_ise", "All I-Ds that are in a particular ISE state"), + ("state_rfceditor", "All I-Ds that are in a particular RFC Editor state"), + ("state_ietf", "All I-Ds that are in a particular Working Group state"), + ("author", "All I-Ds with a particular author"), + ("author_rfc", "All RFCs with a particular author"), + ("ad", "All I-Ds with a particular responsible AD"), + ("shepherd", "All I-Ds with a particular document shepherd"), + ( + "name_contains", + "All I-Ds with particular text/regular expression in the name", + ), ] community_list = ForeignKey(CommunityList) @@ -70,7 +75,9 @@ class SearchRule(models.Model): state = ForeignKey(State, blank=True, null=True) group = ForeignKey(Group, blank=True, null=True) person = ForeignKey(Person, blank=True, null=True) - text = models.CharField(verbose_name="Text/RegExp", max_length=255, blank=True, default="") + text = models.CharField( + verbose_name="Text/RegExp", max_length=255, blank=True, default="" + ) # store a materialized view/index over which documents are matched # by the name_contains rule to avoid having to scan the whole @@ -79,7 +86,15 @@ class SearchRule(models.Model): name_contains_index = models.ManyToManyField(Document) def __str__(self): - return "%s %s %s/%s/%s/%s" % (self.community_list, self.rule_type, self.state, self.group, self.person, self.text) + return "%s %s %s/%s/%s/%s" % ( + self.community_list, + self.rule_type, + self.state, + self.group, + self.person, + self.text, + ) + class EmailSubscription(models.Model): community_list = ForeignKey(CommunityList) @@ -87,9 +102,15 @@ class EmailSubscription(models.Model): NOTIFICATION_CHOICES = [ ("all", "All changes"), - ("significant", "Only significant state changes") + ("significant", "Only significant state changes"), ] - notify_on = models.CharField(max_length=30, choices=NOTIFICATION_CHOICES, default="all") + notify_on = models.CharField( + max_length=30, choices=NOTIFICATION_CHOICES, default="all" + ) def __str__(self): - return "%s to %s (%s changes)" % (self.email, self.community_list, self.notify_on) + return "%s to %s (%s changes)" % ( + self.email, + self.community_list, + self.notify_on, + ) diff --git a/ietf/community/resources.py b/ietf/community/resources.py index 2d87f3ce90..ff7faf105f 100644 --- a/ietf/community/resources.py +++ b/ietf/community/resources.py @@ -16,17 +16,22 @@ from ietf.doc.resources import DocumentResource from ietf.group.resources import GroupResource from ietf.utils.resources import UserResource + + class CommunityListResource(ModelResource): - user = ToOneField(UserResource, 'user', null=True) - group = ToOneField(GroupResource, 'group', null=True) - added_docs = ToManyField(DocumentResource, 'added_docs', null=True) + user = ToOneField(UserResource, "user", null=True) + group = ToOneField(GroupResource, "group", null=True) + added_docs = ToManyField(DocumentResource, "added_docs", null=True) + class Meta: cache = SimpleCache() queryset = CommunityList.objects.all() serializer = api.Serializer() - #resource_name = 'communitylist' - ordering = ['id', ] - filtering = { + # resource_name = 'communitylist' + ordering = [ + "id", + ] + filtering = { "id": ALL, "secret": ALL, "cached": ALL, @@ -34,35 +39,49 @@ class Meta: "group": ALL_WITH_RELATIONS, "added_docs": ALL_WITH_RELATIONS, } + + api.community.register(CommunityListResource()) + class SearchRuleResource(ModelResource): - community_list = ToOneField(CommunityListResource, 'community_list') + community_list = ToOneField(CommunityListResource, "community_list") + class Meta: cache = SimpleCache() queryset = SearchRule.objects.all() serializer = api.Serializer() - #resource_name = 'rule' - ordering = ['id', ] - filtering = { + # resource_name = 'rule' + ordering = [ + "id", + ] + filtering = { "id": ALL, "rule_type": ALL, "community_list": ALL_WITH_RELATIONS, } + + api.community.register(SearchRuleResource()) + class EmailSubscriptionResource(ModelResource): - community_list = ToOneField(CommunityListResource, 'community_list') + community_list = ToOneField(CommunityListResource, "community_list") + class Meta: cache = SimpleCache() queryset = EmailSubscription.objects.all() serializer = api.Serializer() - #resource_name = 'emailsubscription' - ordering = ['id', ] - filtering = { + # resource_name = 'emailsubscription' + ordering = [ + "id", + ] + filtering = { "id": ALL, "email": ALL_WITH_RELATIONS, "notify_on": ALL, "community_list": ALL_WITH_RELATIONS, } + + api.community.register(EmailSubscriptionResource()) diff --git a/ietf/community/tasks.py b/ietf/community/tasks.py index 763a596495..0ef5380b7a 100644 --- a/ietf/community/tasks.py +++ b/ietf/community/tasks.py @@ -1,15 +1,18 @@ # Copyright The IETF Trust 2024, All Rights Reserved from celery import shared_task -from ietf.doc.models import DocEvent +from ietf.doc.models import DocEvent from ietf.utils.log import log @shared_task def notify_event_to_subscribers_task(event_id): from .utils import notify_event_to_subscribers + event = DocEvent.objects.filter(pk=event_id).first() if event is None: - log(f"Unable to send subscriber notifications because DocEvent {event_id} was not found") + log( + f"Unable to send subscriber notifications because DocEvent {event_id} was not found" + ) else: notify_event_to_subscribers(event) diff --git a/ietf/community/tests.py b/ietf/community/tests.py index 9bd7789958..b77c3713d0 100644 --- a/ietf/community/tests.py +++ b/ietf/community/tests.py @@ -7,12 +7,18 @@ from django.test.utils import override_settings from django.urls import reverse as urlreverse -import debug # pyflakes:ignore +import debug # pyflakes:ignore from ietf.community.models import CommunityList, SearchRule, EmailSubscription from ietf.community.signals import notify_of_event -from ietf.community.utils import docs_matching_community_list_rule, community_list_rules_matching_doc -from ietf.community.utils import reset_name_contains_index_for_rule, notify_event_to_subscribers +from ietf.community.utils import ( + docs_matching_community_list_rule, + community_list_rules_matching_doc, +) +from ietf.community.utils import ( + reset_name_contains_index_for_rule, + notify_event_to_subscribers, +) from ietf.community.tasks import notify_event_to_subscribers_task import ietf.community.views from ietf.group.models import Group @@ -26,35 +32,80 @@ from ietf.group.factories import GroupFactory, RoleFactory from ietf.person.factories import PersonFactory, EmailFactory, AliasFactory + class CommunityListTests(TestCase): def test_rule_matching(self): - plain = PersonFactory(user__username='plain') - ad = Person.objects.get(user__username='ad') + plain = PersonFactory(user__username="plain") + ad = Person.objects.get(user__username="ad") draft = WgDraftFactory( - group__parent=Group.objects.get(acronym='farfut' ), + group__parent=Group.objects.get(acronym="farfut"), authors=[ad], ad=ad, shepherd=plain.email(), - states=[('draft-iesg','lc'),('draft','active')], + states=[("draft-iesg", "lc"), ("draft", "active")], ) clist = CommunityList.objects.create(person=plain) - rule_group = SearchRule.objects.create(rule_type="group", group=draft.group, state=State.objects.get(type="draft", slug="active"), community_list=clist) - rule_group_rfc = SearchRule.objects.create(rule_type="group_rfc", group=draft.group, state=State.objects.get(type="rfc", slug="published"), community_list=clist) - rule_area = SearchRule.objects.create(rule_type="area", group=draft.group.parent, state=State.objects.get(type="draft", slug="active"), community_list=clist) + rule_group = SearchRule.objects.create( + rule_type="group", + group=draft.group, + state=State.objects.get(type="draft", slug="active"), + community_list=clist, + ) + rule_group_rfc = SearchRule.objects.create( + rule_type="group_rfc", + group=draft.group, + state=State.objects.get(type="rfc", slug="published"), + community_list=clist, + ) + rule_area = SearchRule.objects.create( + rule_type="area", + group=draft.group.parent, + state=State.objects.get(type="draft", slug="active"), + community_list=clist, + ) - rule_state_iesg = SearchRule.objects.create(rule_type="state_iesg", state=State.objects.get(type="draft-iesg", slug="lc"), community_list=clist) + rule_state_iesg = SearchRule.objects.create( + rule_type="state_iesg", + state=State.objects.get(type="draft-iesg", slug="lc"), + community_list=clist, + ) - rule_author = SearchRule.objects.create(rule_type="author", state=State.objects.get(type="draft", slug="active"), person=Person.objects.filter(documentauthor__document=draft).first(), community_list=clist) + rule_author = SearchRule.objects.create( + rule_type="author", + state=State.objects.get(type="draft", slug="active"), + person=Person.objects.filter(documentauthor__document=draft).first(), + community_list=clist, + ) - rule_ad = SearchRule.objects.create(rule_type="ad", state=State.objects.get(type="draft", slug="active"), person=draft.ad, community_list=clist) + rule_ad = SearchRule.objects.create( + rule_type="ad", + state=State.objects.get(type="draft", slug="active"), + person=draft.ad, + community_list=clist, + ) - rule_shepherd = SearchRule.objects.create(rule_type="shepherd", state=State.objects.get(type="draft", slug="active"), person=draft.shepherd.person, community_list=clist) + rule_shepherd = SearchRule.objects.create( + rule_type="shepherd", + state=State.objects.get(type="draft", slug="active"), + person=draft.shepherd.person, + community_list=clist, + ) - rule_group_exp = SearchRule.objects.create(rule_type="group_exp", group=draft.group, state=State.objects.get(type="draft", slug="expired"), community_list=clist) + rule_group_exp = SearchRule.objects.create( + rule_type="group_exp", + group=draft.group, + state=State.objects.get(type="draft", slug="expired"), + community_list=clist, + ) - rule_name_contains = SearchRule.objects.create(rule_type="name_contains", state=State.objects.get(type="draft", slug="active"), text="draft-.*" + "-".join(draft.name.split("-")[2:]), community_list=clist) + rule_name_contains = SearchRule.objects.create( + rule_type="name_contains", + state=State.objects.get(type="draft", slug="active"), + text="draft-.*" + "-".join(draft.name.split("-")[2:]), + community_list=clist, + ) reset_name_contains_index_for_rule(rule_name_contains) # doc -> rules @@ -71,29 +122,44 @@ def test_rule_matching(self): # rule -> docs self.assertTrue(draft in list(docs_matching_community_list_rule(rule_group))) - self.assertTrue(draft not in list(docs_matching_community_list_rule(rule_group_rfc))) + self.assertTrue( + draft not in list(docs_matching_community_list_rule(rule_group_rfc)) + ) self.assertTrue(draft in list(docs_matching_community_list_rule(rule_area))) - self.assertTrue(draft in list(docs_matching_community_list_rule(rule_state_iesg))) + self.assertTrue( + draft in list(docs_matching_community_list_rule(rule_state_iesg)) + ) self.assertTrue(draft in list(docs_matching_community_list_rule(rule_author))) self.assertTrue(draft in list(docs_matching_community_list_rule(rule_ad))) self.assertTrue(draft in list(docs_matching_community_list_rule(rule_shepherd))) - self.assertTrue(draft in list(docs_matching_community_list_rule(rule_name_contains))) - self.assertTrue(draft not in list(docs_matching_community_list_rule(rule_group_exp))) + self.assertTrue( + draft in list(docs_matching_community_list_rule(rule_name_contains)) + ) + self.assertTrue( + draft not in list(docs_matching_community_list_rule(rule_group_exp)) + ) - draft.set_state(State.objects.get(type='draft', slug='expired')) + draft.set_state(State.objects.get(type="draft", slug="expired")) # doc -> rules matching_rules = list(community_list_rules_matching_doc(draft)) self.assertTrue(rule_group_exp in matching_rules) # rule -> docs - self.assertTrue(draft in list(docs_matching_community_list_rule(rule_group_exp))) + self.assertTrue( + draft in list(docs_matching_community_list_rule(rule_group_exp)) + ) def test_view_list_duplicates(self): - person = PersonFactory(name="John Q. Public", user__username="bazquux@example.com") + person = PersonFactory( + name="John Q. Public", user__username="bazquux@example.com" + ) PersonFactory(name="John Q. Public", user__username="foobar@example.com") - url = urlreverse(ietf.community.views.view_list, kwargs={ "email_or_name": person.plain_name()}) + url = urlreverse( + ietf.community.views.view_list, + kwargs={"email_or_name": person.plain_name()}, + ) r = self.client.get(url) self.assertEqual(r.status_code, 404) @@ -104,14 +170,17 @@ def complex_person(self, *args, **kwargs): return person def email_or_name_set(self, person): - return [e for e in Email.objects.filter(person=person)] + \ - [a for a in Alias.objects.filter(person=person)] + return [e for e in Email.objects.filter(person=person)] + [ + a for a in Alias.objects.filter(person=person) + ] def do_view_list_test(self, person): draft = WgDraftFactory() # without list for id in self.email_or_name_set(person): - url = urlreverse(ietf.community.views.view_list, kwargs={ "email_or_name": id }) + url = urlreverse( + ietf.community.views.view_list, kwargs={"email_or_name": id} + ) r = self.client.get(url) self.assertEqual(r.status_code, 200, msg=f"id='{id}', url='{url}'") @@ -126,31 +195,37 @@ def do_view_list_test(self, person): text="test", ) for id in self.email_or_name_set(person): - url = urlreverse(ietf.community.views.view_list, kwargs={ "email_or_name": id }) + url = urlreverse( + ietf.community.views.view_list, kwargs={"email_or_name": id} + ) r = self.client.get(url) self.assertEqual(r.status_code, 200, msg=f"id='{id}', url='{url}'") self.assertContains(r, draft.name) def test_view_list(self): - person = self.complex_person(user__username='plain') + person = self.complex_person(user__username="plain") self.do_view_list_test(person) - + def test_view_list_without_active_email(self): - person = self.complex_person(user__username='plain') + person = self.complex_person(user__username="plain") person.email_set.update(active=False) self.do_view_list_test(person) def test_manage_personal_list(self): - person = self.complex_person(user__username='plain') - ad = Person.objects.get(user__username='ad') + person = self.complex_person(user__username="plain") + ad = Person.objects.get(user__username="ad") draft = WgDraftFactory(authors=[ad]) - url = urlreverse(ietf.community.views.manage_list, kwargs={ "email_or_name": person.email() }) + url = urlreverse( + ietf.community.views.manage_list, kwargs={"email_or_name": person.email()} + ) login_testing_unauthorized(self, "plain", url) for id in self.email_or_name_set(person): - url = urlreverse(ietf.community.views.manage_list, kwargs={ "email_or_name": id }) - r = self.client.get(url, user='plain') + url = urlreverse( + ietf.community.views.manage_list, kwargs={"email_or_name": id} + ) + r = self.client.get(url, user="plain") self.assertEqual(r.status_code, 200, msg=f"id='{id}', url='{url}'") # We can't call post() with follow=True because that 404's if @@ -158,11 +233,13 @@ def test_manage_personal_list(self): # apparently re-encodes the already-encoded url. def follow(r): redirect_url = r.url or url - return self.client.get(redirect_url, user='plain') + return self.client.get(redirect_url, user="plain") # add document - self.assertContains(r, 'add_document') - r = self.client.post(url, {'action': 'add_documents', 'documents': draft.pk}) + self.assertContains(r, "add_document") + r = self.client.post( + url, {"action": "add_documents", "documents": draft.pk} + ) self.assertEqual(r.status_code, 302, msg=f"id='{id}', url='{url}'") clist = CommunityList.objects.get(person__user__username="plain") self.assertTrue(clist.added_docs.filter(pk=draft.pk)) @@ -170,8 +247,10 @@ def follow(r): self.assertContains(r, draft.name, status_code=200) # remove document - self.assertContains(r, 'remove_document_%s' % draft.pk) - r = self.client.post(url, {'action': 'remove_document', 'document': draft.pk}) + self.assertContains(r, "remove_document_%s" % draft.pk) + r = self.client.post( + url, {"action": "remove_document", "document": draft.pk} + ) self.assertEqual(r.status_code, 302, msg=f"id='{id}', url='{url}'") clist = CommunityList.objects.get(person__user__username="plain") self.assertTrue(not clist.added_docs.filter(pk=draft.pk)) @@ -179,23 +258,37 @@ def follow(r): self.assertNotContains(r, draft.name, status_code=200) # add rule - r = self.client.post(url, { - "action": "add_rule", - "rule_type": "author_rfc", - "author_rfc-person": Person.objects.filter(documentauthor__document=draft).first().pk, - "author_rfc-state": State.objects.get(type="rfc", slug="published").pk, - }) + r = self.client.post( + url, + { + "action": "add_rule", + "rule_type": "author_rfc", + "author_rfc-person": Person.objects.filter( + documentauthor__document=draft + ) + .first() + .pk, + "author_rfc-state": State.objects.get( + type="rfc", slug="published" + ).pk, + }, + ) self.assertEqual(r.status_code, 302, msg=f"id='{id}', url='{url}'") clist = CommunityList.objects.get(person__user__username="plain") self.assertTrue(clist.searchrule_set.filter(rule_type="author_rfc")) # add name_contains rule - r = self.client.post(url, { - "action": "add_rule", - "rule_type": "name_contains", - "name_contains-text": "draft.*mars", - "name_contains-state": State.objects.get(type="draft", slug="active").pk, - }) + r = self.client.post( + url, + { + "action": "add_rule", + "rule_type": "name_contains", + "name_contains-text": "draft.*mars", + "name_contains-state": State.objects.get( + type="draft", slug="active" + ).pk, + }, + ) self.assertEqual(r.status_code, 302, msg=f"id='{id}', url='{url}'") clist = CommunityList.objects.get(person__user__username="plain") self.assertTrue(clist.searchrule_set.filter(rule_type="name_contains")) @@ -205,22 +298,31 @@ def follow(r): self.assertEqual(r.status_code, 200, msg=f"id='{id}', url='{url}'") rule = clist.searchrule_set.filter(rule_type="author_rfc").first() q = PyQuery(r.content) - self.assertEqual(len(q('#r%s' % rule.pk)), 1) + self.assertEqual(len(q("#r%s" % rule.pk)), 1) # remove rule - r = self.client.post(url, { - "action": "remove_rule", - "rule": rule.pk, - }) + r = self.client.post( + url, + { + "action": "remove_rule", + "rule": rule.pk, + }, + ) clist = CommunityList.objects.get(person__user__username="plain") self.assertTrue(not clist.searchrule_set.filter(rule_type="author_rfc")) def test_manage_group_list(self): - draft = WgDraftFactory(group__acronym='mars') - RoleFactory(group__acronym='mars',name_id='chair',person=PersonFactory(user__username='marschairman')) + draft = WgDraftFactory(group__acronym="mars") + RoleFactory( + group__acronym="mars", + name_id="chair", + person=PersonFactory(user__username="marschairman"), + ) - url = urlreverse(ietf.community.views.manage_list, kwargs={ "acronym": draft.group.acronym }) + url = urlreverse( + ietf.community.views.manage_list, kwargs={"acronym": draft.group.acronym} + ) setup_default_community_list_for_group(draft.group) login_testing_unauthorized(self, "marschairman", url) @@ -229,27 +331,41 @@ def test_manage_group_list(self): self.assertEqual(r.status_code, 200) # Verify GET also works with non-WG and RG groups - for gtype in ['area','program']: + for gtype in ["area", "program"]: g = GroupFactory.create(type_id=gtype) # make sure the group's features have been initialized to improve coverage - _ = g.features # pyflakes:ignore + _ = g.features # pyflakes:ignore p = PersonFactory() - g.role_set.create(name_id={'area':'ad','program':'lead'}[gtype],person=p, email=p.email()) - url = urlreverse(ietf.community.views.manage_list, kwargs={ "acronym": g.acronym }) + g.role_set.create( + name_id={"area": "ad", "program": "lead"}[gtype], + person=p, + email=p.email(), + ) + url = urlreverse( + ietf.community.views.manage_list, kwargs={"acronym": g.acronym} + ) setup_default_community_list_for_group(g) - self.client.login(username=p.user.username,password=p.user.username+"+password") + self.client.login( + username=p.user.username, password=p.user.username + "+password" + ) r = self.client.get(url) self.assertEqual(r.status_code, 200) def test_track_untrack_document(self): - person = self.complex_person(user__username='plain') + person = self.complex_person(user__username="plain") draft = WgDraftFactory() - url = urlreverse(ietf.community.views.track_document, kwargs={ "email_or_name": person.email(), "name": draft.name }) + url = urlreverse( + ietf.community.views.track_document, + kwargs={"email_or_name": person.email(), "name": draft.name}, + ) login_testing_unauthorized(self, "plain", url) for id in self.email_or_name_set(person): - url = urlreverse(ietf.community.views.track_document, kwargs={ "email_or_name": id, "name": draft.name }) + url = urlreverse( + ietf.community.views.track_document, + kwargs={"email_or_name": id, "name": draft.name}, + ) # track r = self.client.get(url) @@ -261,7 +377,10 @@ def test_track_untrack_document(self): self.assertEqual(list(clist.added_docs.all()), [draft]) # untrack - url = urlreverse(ietf.community.views.untrack_document, kwargs={ "email_or_name": id, "name": draft.name }) + url = urlreverse( + ietf.community.views.untrack_document, + kwargs={"email_or_name": id, "name": draft.name}, + ) r = self.client.get(url) self.assertEqual(r.status_code, 200, msg=f"id='{id}', url='{url}'") @@ -271,36 +390,47 @@ def test_track_untrack_document(self): self.assertEqual(list(clist.added_docs.all()), []) def test_track_untrack_document_through_ajax(self): - person = self.complex_person(user__username='plain') + person = self.complex_person(user__username="plain") draft = WgDraftFactory() - url = urlreverse(ietf.community.views.track_document, kwargs={ "email_or_name": person.email(), "name": draft.name }) + url = urlreverse( + ietf.community.views.track_document, + kwargs={"email_or_name": person.email(), "name": draft.name}, + ) login_testing_unauthorized(self, "plain", url) for id in self.email_or_name_set(person): - url = urlreverse(ietf.community.views.track_document, kwargs={ "email_or_name": id, "name": draft.name }) + url = urlreverse( + ietf.community.views.track_document, + kwargs={"email_or_name": id, "name": draft.name}, + ) # track - r = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + r = self.client.post(url, HTTP_X_REQUESTED_WITH="XMLHttpRequest") self.assertEqual(r.status_code, 200, msg=f"id='{id}', url='{url}'") self.assertEqual(r.json()["success"], True) clist = CommunityList.objects.get(person__user__username="plain") self.assertEqual(list(clist.added_docs.all()), [draft]) # untrack - url = urlreverse(ietf.community.views.untrack_document, kwargs={ "email_or_name": id, "name": draft.name }) - r = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + url = urlreverse( + ietf.community.views.untrack_document, + kwargs={"email_or_name": id, "name": draft.name}, + ) + r = self.client.post(url, HTTP_X_REQUESTED_WITH="XMLHttpRequest") self.assertEqual(r.status_code, 200, msg=f"id='{id}', url='{url}'") self.assertEqual(r.json()["success"], True) clist = CommunityList.objects.get(person__user__username="plain") self.assertEqual(list(clist.added_docs.all()), []) def test_csv(self): - person = self.complex_person(user__username='plain') + person = self.complex_person(user__username="plain") draft = WgDraftFactory() for id in self.email_or_name_set(person): - url = urlreverse(ietf.community.views.export_to_csv, kwargs={ "email_or_name": id }) + url = urlreverse( + ietf.community.views.export_to_csv, kwargs={"email_or_name": id} + ) # without list r = self.client.get(url) @@ -324,7 +454,9 @@ def test_csv(self): def test_csv_for_group(self): draft = WgDraftFactory() - url = urlreverse(ietf.community.views.export_to_csv, kwargs={ "acronym": draft.group.acronym }) + url = urlreverse( + ietf.community.views.export_to_csv, kwargs={"acronym": draft.group.acronym} + ) setup_default_community_list_for_group(draft.group) @@ -333,11 +465,11 @@ def test_csv_for_group(self): self.assertEqual(r.status_code, 200) def test_feed(self): - person = self.complex_person(user__username='plain') + person = self.complex_person(user__username="plain") draft = WgDraftFactory() for id in self.email_or_name_set(person): - url = urlreverse(ietf.community.views.feed, kwargs={ "email_or_name": id }) + url = urlreverse(ietf.community.views.feed, kwargs={"email_or_name": id}) # without list r = self.client.get(url) @@ -360,28 +492,34 @@ def test_feed(self): # only significant r = self.client.get(url + "?significant=1") self.assertEqual(r.status_code, 200, msg=f"id='{id}', url='{url}'") - self.assertNotContains(r, '') + self.assertNotContains(r, "") def test_feed_for_group(self): draft = WgDraftFactory() - url = urlreverse(ietf.community.views.feed, kwargs={ "acronym": draft.group.acronym }) + url = urlreverse( + ietf.community.views.feed, kwargs={"acronym": draft.group.acronym} + ) setup_default_community_list_for_group(draft.group) # test GET, rest is tested with personal list r = self.client.get(url) self.assertEqual(r.status_code, 200) - + def test_subscription(self): - person = self.complex_person(user__username='plain') + person = self.complex_person(user__username="plain") draft = WgDraftFactory() - url = urlreverse(ietf.community.views.subscription, kwargs={ "email_or_name": person.email() }) + url = urlreverse( + ietf.community.views.subscription, kwargs={"email_or_name": person.email()} + ) login_testing_unauthorized(self, "plain", url) for id in self.email_or_name_set(person): - url = urlreverse(ietf.community.views.subscription, kwargs={ "email_or_name": id }) + url = urlreverse( + ietf.community.views.subscription, kwargs={"email_or_name": id} + ) # subscription without list r = self.client.get(url) @@ -399,29 +537,49 @@ def test_subscription(self): ) for email in Email.objects.filter(person=person): - url = urlreverse(ietf.community.views.subscription, kwargs={ "email_or_name": email }) + url = urlreverse( + ietf.community.views.subscription, kwargs={"email_or_name": email} + ) r = self.client.get(url) self.assertEqual(r.status_code, 200) # subscribe - r = self.client.post(url, { "email": email.pk, "notify_on": "significant", "action": "subscribe" }) + r = self.client.post( + url, + {"email": email.pk, "notify_on": "significant", "action": "subscribe"}, + ) self.assertEqual(r.status_code, 302) - subscription = EmailSubscription.objects.filter(community_list=clist, email=email, notify_on="significant").first() + subscription = EmailSubscription.objects.filter( + community_list=clist, email=email, notify_on="significant" + ).first() self.assertTrue(subscription) # delete subscription - r = self.client.post(url, { "subscription_id": subscription.pk, "action": "unsubscribe" }) + r = self.client.post( + url, {"subscription_id": subscription.pk, "action": "unsubscribe"} + ) self.assertEqual(r.status_code, 302) - self.assertEqual(EmailSubscription.objects.filter(community_list=clist, email=email, notify_on="significant").count(), 0) + self.assertEqual( + EmailSubscription.objects.filter( + community_list=clist, email=email, notify_on="significant" + ).count(), + 0, + ) def test_subscription_for_group(self): - draft = WgDraftFactory(group__acronym='mars') - RoleFactory(group__acronym='mars',name_id='chair',person=PersonFactory(user__username='marschairman')) + draft = WgDraftFactory(group__acronym="mars") + RoleFactory( + group__acronym="mars", + name_id="chair", + person=PersonFactory(user__username="marschairman"), + ) - url = urlreverse(ietf.community.views.subscription, kwargs={ "acronym": draft.group.acronym }) + url = urlreverse( + ietf.community.views.subscription, kwargs={"acronym": draft.group.acronym} + ) setup_default_community_list_for_group(draft.group) @@ -434,7 +592,7 @@ def test_subscription_for_group(self): @mock.patch("ietf.community.signals.notify_of_event") def test_notification_signal_receiver(self, mock_notify_of_event): """Saving a newly created DocEvent should notify subscribers - + This implicitly tests that notify_of_event_receiver is hooked up to the post_save signal. """ # Arbitrary model that's not a DocEvent @@ -442,19 +600,26 @@ def test_notification_signal_receiver(self, mock_notify_of_event): mock_notify_of_event.reset_mock() # clear any calls that resulted from the factories person.save() self.assertFalse(mock_notify_of_event.called) - + # build a DocEvent that is not yet persisted doc = DocumentFactory() event = DocEventFactory.build(by=person, doc=doc) # builds but does not save... mock_notify_of_event.reset_mock() # clear any calls that resulted from the factories event.save() - self.assertEqual(mock_notify_of_event.call_count, 1, "notify_task should be run on creation of DocEvent") + self.assertEqual( + mock_notify_of_event.call_count, + 1, + "notify_task should be run on creation of DocEvent", + ) self.assertEqual(mock_notify_of_event.call_args, mock.call(event)) - # save the existing DocEvent and see that no notification is sent + # save the existing DocEvent and see that no notification is sent mock_notify_of_event.reset_mock() event.save() - self.assertFalse(mock_notify_of_event.called, "notify_task should not be run save of on existing DocEvent") + self.assertFalse( + mock_notify_of_event.called, + "notify_task should not be run save of on existing DocEvent", + ) # Mock out the on_commit call so we can tell whether the task was actually queued @mock.patch("ietf.submit.views.transaction.on_commit", side_effect=lambda x: x()) @@ -468,7 +633,10 @@ def test_notify_of_event(self, mock_notify_task, mock_on_commit): # under test does not make this call when in "test" mode with override_settings(SERVER_MODE="not-test"): notify_of_event(event) - self.assertTrue(mock_notify_task.delay.called, "notify_task should run for a DocEvent on a draft") + self.assertTrue( + mock_notify_task.delay.called, + "notify_task should run for a DocEvent on a draft", + ) mock_notify_task.reset_mock() event.skip_community_list_notification = True @@ -476,18 +644,24 @@ def test_notify_of_event(self, mock_notify_task, mock_on_commit): # under test does not make this call when in "test" mode with override_settings(SERVER_MODE="not-test"): notify_of_event(event) - self.assertFalse(mock_notify_task.delay.called, "notify_task should not run when skip_community_list_notification is set") + self.assertFalse( + mock_notify_task.delay.called, + "notify_task should not run when skip_community_list_notification is set", + ) event = DocEventFactory.build(by=person, doc=DocumentFactory(type_id="rfc")) # be careful overriding SERVER_MODE - we do it here because the method # under test does not make this call when in "test" mode with override_settings(SERVER_MODE="not-test"): notify_of_event(event) - self.assertFalse(mock_notify_task.delay.called, "notify_task should not run on a document with type 'rfc'") + self.assertFalse( + mock_notify_task.delay.called, + "notify_task should not run on a document with type 'rfc'", + ) @mock.patch("ietf.utils.mail.send_mail_text") def test_notify_event_to_subscribers(self, mock_send_mail_text): - person = PersonFactory(user__username='plain') + person = PersonFactory(user__username="plain") draft = WgDraftFactory() clist = CommunityList.objects.create(person=person) @@ -522,11 +696,13 @@ def test_notify_event_to_subscribers(self, mock_send_mail_text): mock_send_mail_text.reset_mock() notify_event_to_subscribers(event) self.assertEqual(mock_send_mail_text.call_count, 2) - addresses = [call_args[0][1] for call_args in mock_send_mail_text.call_args_list] + addresses = [ + call_args[0][1] for call_args in mock_send_mail_text.call_args_list + ] subjects = {call_args[0][3] for call_args in mock_send_mail_text.call_args_list} contents = {call_args[0][4] for call_args in mock_send_mail_text.call_args_list} self.assertCountEqual( - addresses, + addresses, [sub_to_significant.email.address, sub_to_all.email.address], ) self.assertEqual(len(subjects), 1) @@ -545,4 +721,3 @@ def test_notify_event_to_subscribers_task(self, mock_notify): d.delete() notify_event_to_subscribers_task(event_id=d.pk) self.assertFalse(mock_notify.called) - diff --git a/ietf/community/urls.py b/ietf/community/urls.py index 3ab132f2dc..a9ff56d641 100644 --- a/ietf/community/urls.py +++ b/ietf/community/urls.py @@ -1,14 +1,18 @@ - - from ietf.community import views from ietf.utils.urls import url urlpatterns = [ - url(r'^personal/(?P[^/]+)/$', views.view_list), - url(r'^personal/(?P[^/]+)/manage/$', views.manage_list), - url(r'^personal/(?P[^/]+)/trackdocument/(?P[^/]+)/$', views.track_document), - url(r'^personal/(?P[^/]+)/untrackdocument/(?P[^/]+)/$', views.untrack_document), - url(r'^personal/(?P[^/]+)/csv/$', views.export_to_csv), - url(r'^personal/(?P[^/]+)/feed/$', views.feed), - url(r'^personal/(?P[^/]+)/subscription/$', views.subscription), + url(r"^personal/(?P[^/]+)/$", views.view_list), + url(r"^personal/(?P[^/]+)/manage/$", views.manage_list), + url( + r"^personal/(?P[^/]+)/trackdocument/(?P[^/]+)/$", + views.track_document, + ), + url( + r"^personal/(?P[^/]+)/untrackdocument/(?P[^/]+)/$", + views.untrack_document, + ), + url(r"^personal/(?P[^/]+)/csv/$", views.export_to_csv), + url(r"^personal/(?P[^/]+)/feed/$", views.feed), + url(r"^personal/(?P[^/]+)/subscription/$", views.subscription), ] diff --git a/ietf/community/utils.py b/ietf/community/utils.py index f23e8d26ab..d699820216 100644 --- a/ietf/community/utils.py +++ b/ietf/community/utils.py @@ -7,7 +7,7 @@ from django.db.models import Q from django.conf import settings -import debug # pyflakes:ignore +import debug # pyflakes:ignore from ietf.community.models import CommunityList, EmailSubscription, SearchRule from ietf.doc.models import Document, State @@ -17,16 +17,27 @@ from ietf.utils.mail import send_mail + def states_of_significant_change(): return State.objects.filter(used=True).filter( - Q(type="draft-stream-ietf", slug__in=['adopt-wg', 'wg-lc', 'writeupw', 'parked', 'dead']) | - Q(type="draft-iesg", slug__in=['pub-req', 'lc', 'iesg-eva', 'rfcqueue']) | - Q(type="draft-stream-iab", slug__in=['active', 'review-c', 'rfc-edit']) | - Q(type="draft-stream-irtf", slug__in=['active', 'rg-lc', 'irsg-w', 'iesg-rev', 'rfc-edit', 'iesghold']) | - Q(type="draft-stream-ise", slug__in=['receive', 'ise-rev', 'iesg-rev', 'rfc-edit', 'iesghold']) | - Q(type="draft", slug__in=['rfc', 'dead']) + Q( + type="draft-stream-ietf", + slug__in=["adopt-wg", "wg-lc", "writeupw", "parked", "dead"], + ) + | Q(type="draft-iesg", slug__in=["pub-req", "lc", "iesg-eva", "rfcqueue"]) + | Q(type="draft-stream-iab", slug__in=["active", "review-c", "rfc-edit"]) + | Q( + type="draft-stream-irtf", + slug__in=["active", "rg-lc", "irsg-w", "iesg-rev", "rfc-edit", "iesghold"], + ) + | Q( + type="draft-stream-ise", + slug__in=["receive", "ise-rev", "iesg-rev", "rfc-edit", "iesghold"], + ) + | Q(type="draft", slug__in=["rfc", "dead"]) ) + def can_manage_community_list(user, clist): if not user or not user.is_authenticated: return False @@ -34,20 +45,33 @@ def can_manage_community_list(user, clist): if clist.person: return user == clist.person.user elif clist.group: - if has_role(user, 'Secretariat'): + if has_role(user, "Secretariat"): return True - if clist.group.type_id in ['area', 'wg', 'rg', 'ag', 'rag', 'program', ]: - return Role.objects.filter(name__slug__in=clist.group.features.groupman_roles, person__user=user, group=clist.group).exists() + if clist.group.type_id in [ + "area", + "wg", + "rg", + "ag", + "rag", + "program", + ]: + return Role.objects.filter( + name__slug__in=clist.group.features.groupman_roles, + person__user=user, + group=clist.group, + ).exists() return False + def reset_name_contains_index_for_rule(rule): if not rule.rule_type == "name_contains": return rule.name_contains_index.set(Document.objects.filter(name__regex=rule.text)) + def update_name_contains_indexes_with_new_doc(doc): for r in SearchRule.objects.filter(rule_type="name_contains"): # in theory we could use the database to do this query, but @@ -60,15 +84,15 @@ def update_name_contains_indexes_with_new_doc(doc): def docs_matching_community_list_rule(rule): docs = Document.objects.all() - + if rule.rule_type.endswith("_rfc"): docs = docs.filter(type_id="rfc") # rule.state is ignored for RFCs else: docs = docs.filter(type_id="draft", states=rule.state) - - if rule.rule_type in ['group', 'area', 'group_rfc', 'area_rfc']: + + if rule.rule_type in ["group", "area", "group_rfc", "area_rfc"]: return docs.filter(Q(group=rule.group_id) | Q(group__parent=rule.group_id)) - elif rule.rule_type in ['group_exp']: + elif rule.rule_type in ["group_exp"]: return docs.filter(group=rule.group_id) elif rule.rule_type.startswith("state_"): return docs @@ -171,31 +195,46 @@ def docs_tracked_by_community_list(clist): doc_ids.update(rfc.pk for rfc in doc.related_that_doc("became_rfc")) for rule in clist.searchrule_set.all(): - doc_ids = doc_ids | set(docs_matching_community_list_rule(rule).values_list("pk", flat=True)) + doc_ids = doc_ids | set( + docs_matching_community_list_rule(rule).values_list("pk", flat=True) + ) return Document.objects.filter(pk__in=doc_ids) + def community_lists_tracking_doc(doc): - return CommunityList.objects.filter(Q(added_docs=doc) | Q(searchrule__in=community_list_rules_matching_doc(doc))) + return CommunityList.objects.filter( + Q(added_docs=doc) | Q(searchrule__in=community_list_rules_matching_doc(doc)) + ) def notify_event_to_subscribers(event): try: - significant = event.type == "changed_state" and event.state_id in [s.pk for s in states_of_significant_change()] + significant = event.type == "changed_state" and event.state_id in [ + s.pk for s in states_of_significant_change() + ] except AttributeError: significant = False - subscriptions = EmailSubscription.objects.filter(community_list__in=community_lists_tracking_doc(event.doc)).distinct() + subscriptions = EmailSubscription.objects.filter( + community_list__in=community_lists_tracking_doc(event.doc) + ).distinct() if not significant: subscriptions = subscriptions.filter(notify_on="all") for sub in subscriptions.select_related("community_list", "email"): clist = sub.community_list - subject = '%s notification: Changes to %s' % (clist.long_name(), event.doc.name) - - send_mail(None, sub.email.address, settings.DEFAULT_FROM_EMAIL, subject, 'community/notification_email.txt', - context = { - 'event': event, - 'clist': clist, - }) + subject = "%s notification: Changes to %s" % (clist.long_name(), event.doc.name) + + send_mail( + None, + sub.email.address, + settings.DEFAULT_FROM_EMAIL, + subject, + "community/notification_email.txt", + context={ + "event": event, + "clist": clist, + }, + ) diff --git a/ietf/community/views.py b/ietf/community/views.py index 923ec556f3..64cfb99e21 100644 --- a/ietf/community/views.py +++ b/ietf/community/views.py @@ -13,13 +13,24 @@ from django.utils import timezone from django.utils.html import strip_tags -import debug # pyflakes:ignore +import debug # pyflakes:ignore from ietf.community.models import CommunityList, EmailSubscription, SearchRule -from ietf.community.forms import SearchRuleTypeForm, SearchRuleForm, AddDocumentsForm, SubscriptionForm +from ietf.community.forms import ( + SearchRuleTypeForm, + SearchRuleForm, + AddDocumentsForm, + SubscriptionForm, +) from ietf.community.utils import can_manage_community_list -from ietf.community.utils import docs_tracked_by_community_list, docs_matching_community_list_rule -from ietf.community.utils import states_of_significant_change, reset_name_contains_index_for_rule +from ietf.community.utils import ( + docs_tracked_by_community_list, + docs_matching_community_list_rule, +) +from ietf.community.utils import ( + states_of_significant_change, + reset_name_contains_index_for_rule, +) from ietf.group.models import Group from ietf.doc.models import DocEvent, Document from ietf.doc.utils_search import prepare_document_table @@ -31,43 +42,61 @@ def lookup_community_list(request, email_or_name=None, acronym=None): """Finds a CommunityList for a person or group - + Instantiates an unsaved CommunityList if one is not found. - + If the person or group cannot be found and uniquely identified, raises an Http404 exception """ assert email_or_name or acronym if acronym: group = get_object_or_404(Group, acronym=acronym) - clist = CommunityList.objects.filter(group=group).first() or CommunityList(group=group) + clist = CommunityList.objects.filter(group=group).first() or CommunityList( + group=group + ) else: persons = lookup_persons(email_or_name) if len(persons) > 1: - if hasattr(request.user, 'person') and request.user.person in persons: + if hasattr(request.user, "person") and request.user.person in persons: person = request.user.person else: - raise Http404(f"Unable to identify the CommunityList for {email_or_name}") + raise Http404( + f"Unable to identify the CommunityList for {email_or_name}" + ) else: person = persons[0] - clist = CommunityList.objects.filter(person=person).first() or CommunityList(person=person) + clist = CommunityList.objects.filter(person=person).first() or CommunityList( + person=person + ) return clist + def view_list(request, email_or_name=None): clist = lookup_community_list(request, email_or_name) # may raise Http404 docs = docs_tracked_by_community_list(clist) docs, meta = prepare_document_table(request, docs, request.GET) - subscribed = request.user.is_authenticated and (EmailSubscription.objects.none() if clist.pk is None else EmailSubscription.objects.filter(community_list=clist, email__person__user=request.user)) + subscribed = request.user.is_authenticated and ( + EmailSubscription.objects.none() + if clist.pk is None + else EmailSubscription.objects.filter( + community_list=clist, email__person__user=request.user + ) + ) + + return render( + request, + "community/view_list.html", + { + "clist": clist, + "docs": docs, + "meta": meta, + "can_manage_list": can_manage_community_list(request.user, clist), + "subscribed": subscribed, + "email_or_name": email_or_name, + }, + ) - return render(request, 'community/view_list.html', { - 'clist': clist, - 'docs': docs, - 'meta': meta, - 'can_manage_list': can_manage_community_list(request.user, clist), - 'subscribed': subscribed, - "email_or_name": email_or_name, - }) @login_required @ignore_view_kwargs("group_type") @@ -79,15 +108,15 @@ def manage_list(request, email_or_name=None, acronym=None): if not can_manage_community_list(request.user, clist): permission_denied(request, "You do not have permission to access this view") - action = request.POST.get('action') + action = request.POST.get("action") - if request.method == 'POST' and action == 'add_documents': + if request.method == "POST" and action == "add_documents": add_doc_form = AddDocumentsForm(request.POST) if add_doc_form.is_valid(): if clist.pk is None: clist.save() - for d in add_doc_form.cleaned_data['documents']: + for d in add_doc_form.cleaned_data["documents"]: if not d in clist.added_docs.all(): clist.added_docs.add(d) @@ -95,8 +124,8 @@ def manage_list(request, email_or_name=None, acronym=None): else: add_doc_form = AddDocumentsForm() - if request.method == 'POST' and action == 'remove_document': - document_id = request.POST.get('document') + if request.method == "POST" and action == "remove_document": + document_id = request.POST.get("document") if clist.pk is not None and document_id: document = get_object_or_404(clist.added_docs, id=document_id) clist.added_docs.remove(document) @@ -104,16 +133,16 @@ def manage_list(request, email_or_name=None, acronym=None): return HttpResponseRedirect("") rule_form = None - if request.method == 'POST' and action == 'add_rule': + if request.method == "POST" and action == "add_rule": rule_type_form = SearchRuleTypeForm(request.POST) if rule_type_form.is_valid(): - rule_type = rule_type_form.cleaned_data['rule_type'] + rule_type = rule_type_form.cleaned_data["rule_type"] if rule_type: rule_form = SearchRuleForm(clist, rule_type, request.POST) if rule_form.is_valid(): if clist.pk is None: clist.save() - + rule = rule_form.save(commit=False) rule.community_list = clist rule.rule_type = rule_type @@ -125,8 +154,8 @@ def manage_list(request, email_or_name=None, acronym=None): else: rule_type_form = SearchRuleTypeForm() - if request.method == 'POST' and action == 'remove_rule': - rule_pk = request.POST.get('rule') + if request.method == "POST" and action == "remove_rule": + rule_pk = request.POST.get("rule") if clist.pk is not None and rule_pk: rule = get_object_or_404(SearchRule, pk=rule_pk, community_list=clist) rule.delete() @@ -137,23 +166,35 @@ def manage_list(request, email_or_name=None, acronym=None): for r in rules: r.matching_documents_count = docs_matching_community_list_rule(r).count() - empty_rule_forms = { rule_type: SearchRuleForm(clist, rule_type) for rule_type, _ in SearchRule.RULE_TYPES } + empty_rule_forms = { + rule_type: SearchRuleForm(clist, rule_type) + for rule_type, _ in SearchRule.RULE_TYPES + } total_count = docs_tracked_by_community_list(clist).count() - all_forms = [f for f in [rule_type_form, rule_form, add_doc_form, *empty_rule_forms.values()] - if f is not None] - return render(request, 'community/manage_list.html', { - 'clist': clist, - 'rules': rules, - 'individually_added': clist.added_docs.all() if clist.pk is not None else [], - 'rule_type_form': rule_type_form, - 'rule_form': rule_form, - 'empty_rule_forms': empty_rule_forms, - 'total_count': total_count, - 'add_doc_form': add_doc_form, - 'all_forms': all_forms, - }) + all_forms = [ + f + for f in [rule_type_form, rule_form, add_doc_form, *empty_rule_forms.values()] + if f is not None + ] + return render( + request, + "community/manage_list.html", + { + "clist": clist, + "rules": rules, + "individually_added": ( + clist.added_docs.all() if clist.pk is not None else [] + ), + "rule_type_form": rule_type_form, + "rule_form": rule_form, + "empty_rule_forms": empty_rule_forms, + "total_count": total_count, + "add_doc_form": add_doc_form, + "all_forms": all_forms, + }, + ) @login_required @@ -161,7 +202,9 @@ def track_document(request, name, email_or_name=None, acronym=None): doc = get_object_or_404(Document, name=name) if request.method == "POST": - clist = lookup_community_list(request, email_or_name, acronym) # may raise Http404 + clist = lookup_community_list( + request, email_or_name, acronym + ) # may raise Http404 if not can_manage_community_list(request.user, clist): permission_denied(request, "You do not have permission to access this view") @@ -172,13 +215,20 @@ def track_document(request, name, email_or_name=None, acronym=None): clist.added_docs.add(doc) if is_ajax(request): - return HttpResponse(json.dumps({ 'success': True }), content_type='application/json') + return HttpResponse( + json.dumps({"success": True}), content_type="application/json" + ) else: return HttpResponseRedirect(clist.get_absolute_url()) - return render(request, "community/track_document.html", { - "name": doc.name, - }) + return render( + request, + "community/track_document.html", + { + "name": doc.name, + }, + ) + @login_required def untrack_document(request, name, email_or_name=None, acronym=None): @@ -192,28 +242,34 @@ def untrack_document(request, name, email_or_name=None, acronym=None): clist.added_docs.remove(doc) if is_ajax(request): - return HttpResponse(json.dumps({ 'success': True }), content_type='application/json') + return HttpResponse( + json.dumps({"success": True}), content_type="application/json" + ) else: return HttpResponseRedirect(clist.get_absolute_url()) - return render(request, "community/untrack_document.html", { - "name": doc.name, - }) + return render( + request, + "community/untrack_document.html", + { + "name": doc.name, + }, + ) @ignore_view_kwargs("group_type") def export_to_csv(request, email_or_name=None, acronym=None): clist = lookup_community_list(request, email_or_name, acronym) # may raise Http404 - response = HttpResponse(content_type='text/csv') + response = HttpResponse(content_type="text/csv") if clist.group: filename = "%s-draft-list.csv" % clist.group.acronym else: filename = "draft-list.csv" - response['Content-Disposition'] = 'attachment; filename=%s' % filename + response["Content-Disposition"] = "attachment; filename=%s" % filename - writer = csv.writer(response, dialect=csv.excel, delimiter=str(',')) + writer = csv.writer(response, dialect=csv.excel, delimiter=str(",")) header = [ "Name", @@ -226,12 +282,12 @@ def export_to_csv(request, email_or_name=None, acronym=None): ] writer.writerow(header) - docs = docs_tracked_by_community_list(clist).select_related('type', 'group', 'ad') + docs = docs_tracked_by_community_list(clist).select_related("type", "group", "ad") for doc in docs.prefetch_related("states", "tags"): row = [] row.append(doc.name) row.append(doc.title) - e = doc.latest_event(type='new_revision') + e = doc.latest_event(type="new_revision") row.append(e.time.strftime("%Y-%m-%d") if e else "") row.append(strip_tags(doc.friendly_state())) row.append(doc.group.acronym if doc.group else "") @@ -242,39 +298,53 @@ def export_to_csv(request, email_or_name=None, acronym=None): return response + @ignore_view_kwargs("group_type") def feed(request, email_or_name=None, acronym=None): clist = lookup_community_list(request, email_or_name, acronym) # may raise Http404 - significant = request.GET.get('significant', '') == '1' + significant = request.GET.get("significant", "") == "1" - documents = docs_tracked_by_community_list(clist).values_list('pk', flat=True) + documents = docs_tracked_by_community_list(clist).values_list("pk", flat=True) since = timezone.now() - datetime.timedelta(days=14) - events = DocEvent.objects.filter( - doc__id__in=documents, - time__gte=since, - ).distinct().order_by('-time', '-id').select_related("doc") + events = ( + DocEvent.objects.filter( + doc__id__in=documents, + time__gte=since, + ) + .distinct() + .order_by("-time", "-id") + .select_related("doc") + ) if significant: - events = events.filter(type="changed_state", statedocevent__state__in=list(states_of_significant_change())) + events = events.filter( + type="changed_state", + statedocevent__state__in=list(states_of_significant_change()), + ) host = request.get_host() - feed_url = 'https://%s%s' % (host, request.get_full_path()) + feed_url = "https://%s%s" % (host, request.get_full_path()) feed_id = uuid.uuid5(uuid.NAMESPACE_URL, str(feed_url)) - title = '%s RSS Feed' % clist.long_name() + title = "%s RSS Feed" % clist.long_name() if significant: - subtitle = 'Significant document changes' + subtitle = "Significant document changes" else: - subtitle = 'Document changes' - - return render(request, 'community/atom.xml', { - 'clist': clist, - 'entries': events[:50], - 'title': title, - 'subtitle': subtitle, - 'id': feed_id.urn, - 'updated': timezone.now(), - }, content_type='text/xml') + subtitle = "Document changes" + + return render( + request, + "community/atom.xml", + { + "clist": clist, + "entries": events[:50], + "title": title, + "subtitle": subtitle, + "id": feed_id.urn, + "updated": timezone.now(), + }, + content_type="text/xml", + ) @login_required @@ -286,9 +356,11 @@ def subscription(request, email_or_name=None, acronym=None): person = request.user.person - existing_subscriptions = EmailSubscription.objects.filter(community_list=clist, email__person=person) + existing_subscriptions = EmailSubscription.objects.filter( + community_list=clist, email__person=person + ) - if request.method == 'POST': + if request.method == "POST": action = request.POST.get("action") if action == "subscribe": form = SubscriptionForm(person, clist, request.POST) @@ -300,14 +372,20 @@ def subscription(request, email_or_name=None, acronym=None): return HttpResponseRedirect("") elif action == "unsubscribe": - existing_subscriptions.filter(pk=request.POST.get("subscription_id")).delete() + existing_subscriptions.filter( + pk=request.POST.get("subscription_id") + ).delete() return HttpResponseRedirect("") else: form = SubscriptionForm(person, clist) - return render(request, 'community/subscription.html', { - 'clist': clist, - 'form': form, - 'existing_subscriptions': existing_subscriptions, - }) + return render( + request, + "community/subscription.html", + { + "clist": clist, + "form": form, + "existing_subscriptions": existing_subscriptions, + }, + ) diff --git a/ietf/context_processors.py b/ietf/context_processors.py index baa8d7a5d2..eb59debd54 100644 --- a/ietf/context_processors.py +++ b/ietf/context_processors.py @@ -6,48 +6,61 @@ from django.utils import timezone from ietf import __version__, __patch__, __release_branch__, __release_hash__ + def server_mode(request): - return {'server_mode': settings.SERVER_MODE} + return {"server_mode": settings.SERVER_MODE} + def rfcdiff_base_url(request): - return {'rfcdiff_base_url': settings.RFCDIFF_BASE_URL} - + return {"rfcdiff_base_url": settings.RFCDIFF_BASE_URL} + + def python_version(): v = sys.version_info - return "%s.%s.%s" % (v.major, v.minor, v.micro, ) + return "%s.%s.%s" % ( + v.major, + v.minor, + v.micro, + ) + def revision_info(request): return { - 'version_num': __version__, - 'patch' : __patch__, - 'branch' : __release_branch__, - 'git_hash' : __release_hash__, - 'django_version': django.get_version(), - 'python_version': python_version(), - 'bugreport_email': settings.BUG_REPORT_EMAIL, + "version_num": __version__, + "patch": __patch__, + "branch": __release_branch__, + "git_hash": __release_hash__, + "django_version": django.get_version(), + "python_version": python_version(), + "bugreport_email": settings.BUG_REPORT_EMAIL, } + def debug_mark_queries_from_view(request): "Marks the queries which has occurred so far as coming from a view." context_extras = {} - if settings.DEBUG and request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS: + if settings.DEBUG and request.META.get("REMOTE_ADDR") in settings.INTERNAL_IPS: from django.db import connection + for query in connection.queries: - query['loc'] = 'V' # V is for 'view' + query["loc"] = "V" # V is for 'view' return context_extras + def sql_debug(request): - if settings.DEBUG and request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS: - return {'sql_debug': True } + if settings.DEBUG and request.META.get("REMOTE_ADDR") in settings.INTERNAL_IPS: + return {"sql_debug": True} else: - return {'sql_debug': False } - + return {"sql_debug": False} + + def settings_info(request): return { - 'settings': settings, + "settings": settings, } + def timezone_now(request): return { - 'timezone_now': timezone.now(), + "timezone_now": timezone.now(), } diff --git a/ietf/cookies/__init__.py b/ietf/cookies/__init__.py index 57ea650271..9fbc29b378 100644 --- a/ietf/cookies/__init__.py +++ b/ietf/cookies/__init__.py @@ -1,3 +1,2 @@ # Copyright The IETF Trust 2010-2020, All Rights Reserved # coding: latin-1 - diff --git a/ietf/cookies/tests.py b/ietf/cookies/tests.py index b233041675..1639f449cd 100644 --- a/ietf/cookies/tests.py +++ b/ietf/cookies/tests.py @@ -511,4 +511,4 @@ def test_expires_soon_90(self): self.assertListEqual([str("expires_soon")], list(r.cookies.keys())) self.assertRegex(q(r, "full_draft/off"), r"\s*Off\s*") self.assertRegex(q(r, "new_enough/60"), r"\s*60 days\s*") - self.assertRegex(q(r, "expires_soon/90"), r"\s*90 days\s*") \ No newline at end of file + self.assertRegex(q(r, "expires_soon/90"), r"\s*90 days\s*") diff --git a/ietf/cookies/urls.py b/ietf/cookies/urls.py index 1d4fa8be93..9cbfc21f2d 100644 --- a/ietf/cookies/urls.py +++ b/ietf/cookies/urls.py @@ -4,13 +4,13 @@ from ietf.utils.urls import url urlpatterns = [ - url(r'^$', views.preferences), - url(r'^new_enough/(?P.+)$', views.new_enough), - url(r'^new_enough/', views.new_enough), - url(r'^expires_soon/(?P.+)$', views.expires_soon), - url(r'^expires_soon/', views.expires_soon), - url(r'^full_draft/(?P.+)$', views.full_draft), - url(r'^full_draft/', views.full_draft), - url(r'^left_menu/(?P.+)$', views.left_menu), - url(r'^left_menu/', views.left_menu), + url(r"^$", views.preferences), + url(r"^new_enough/(?P.+)$", views.new_enough), + url(r"^new_enough/", views.new_enough), + url(r"^expires_soon/(?P.+)$", views.expires_soon), + url(r"^expires_soon/", views.expires_soon), + url(r"^full_draft/(?P.+)$", views.full_draft), + url(r"^full_draft/", views.full_draft), + url(r"^left_menu/(?P.+)$", views.left_menu), + url(r"^left_menu/", views.left_menu), ] diff --git a/ietf/cookies/views.py b/ietf/cookies/views.py index cf9f237bee..16e7548668 100644 --- a/ietf/cookies/views.py +++ b/ietf/cookies/views.py @@ -5,54 +5,70 @@ from django.shortcuts import render from django.conf import settings -import debug # pyflakes:ignore +import debug # pyflakes:ignore + def preferences(request, **kwargs): preferences = request.COOKIES.copy() new_cookies = {} del_cookies = [] - preferences['defaults'] = settings.USER_PREFERENCE_DEFAULTS + preferences["defaults"] = settings.USER_PREFERENCE_DEFAULTS for key in list(settings.USER_PREFERENCE_DEFAULTS.keys()): if key in kwargs: if kwargs[key] == None: del_cookies += [key] else: # ignore bad kwargs - if key in ['new_enough', 'expires_soon'] and not kwargs[key].isdigit(): + if key in ["new_enough", "expires_soon"] and not kwargs[key].isdigit(): pass - elif key in ['full_draft', 'left_menu'] and not kwargs[key] in ['on', 'off']: + elif key in ["full_draft", "left_menu"] and not kwargs[key] in [ + "on", + "off", + ]: pass else: preferences[key] = new_cookies[key] = kwargs[key] - if not key in preferences or preferences[key] in [None, 'None', ''] or key in del_cookies: + if ( + not key in preferences + or preferences[key] in [None, "None", ""] + or key in del_cookies + ): preferences[key] = settings.USER_PREFERENCE_DEFAULTS[key] # reset bad cookie values - if key in ['new_enough', 'expires_soon'] and not preferences[key].isdigit(): + if key in ["new_enough", "expires_soon"] and not preferences[key].isdigit(): preferences[key] = settings.USER_PREFERENCE_DEFAULTS[key] del_cookies += [key] - elif key in ['full_draft', 'left_menu'] and not preferences[key] in ['on', 'off']: + elif key in ["full_draft", "left_menu"] and not preferences[key] in [ + "on", + "off", + ]: preferences[key] = settings.USER_PREFERENCE_DEFAULTS[key] del_cookies += [key] request.COOKIES.update(preferences) - response = render(request, "cookies/settings.html", preferences ) + response = render(request, "cookies/settings.html", preferences) for key in new_cookies: - response.set_cookie(key, new_cookies[key], + response.set_cookie( + key, + new_cookies[key], max_age=settings.PREFERENCES_COOKIE_AGE, secure=settings.SESSION_COOKIE_SECURE or None, httponly=settings.SESSION_COOKIE_HTTPONLY or None, samesite=settings.SESSION_COOKIE_SAMESITE, ) for key in del_cookies: - response.delete_cookie(key, + response.delete_cookie( + key, secure=settings.SESSION_COOKIE_SECURE or None, httponly=settings.SESSION_COOKIE_HTTPONLY or None, samesite=settings.SESSION_COOKIE_SAMESITE, ) return response + def new_enough(request, days=None): return preferences(request, new_enough=days) + def expires_soon(request, days=None): return preferences(request, expires_soon=days) @@ -60,6 +76,6 @@ def expires_soon(request, days=None): def full_draft(request, enabled=None): return preferences(request, full_draft=enabled) + def left_menu(request, enabled=None): return preferences(request, left_menu=enabled) - diff --git a/ietf/dbtemplate/admin.py b/ietf/dbtemplate/admin.py index 3a6e110c2f..7945cd14d0 100644 --- a/ietf/dbtemplate/admin.py +++ b/ietf/dbtemplate/admin.py @@ -4,8 +4,15 @@ class DBTemplateAdmin(admin.ModelAdmin): - list_display = ('title', 'path',) - search_fields = ('title', 'path', ) - ordering = ('path', ) + list_display = ( + "title", + "path", + ) + search_fields = ( + "title", + "path", + ) + ordering = ("path",) + admin.site.register(DBTemplate, DBTemplateAdmin) diff --git a/ietf/dbtemplate/factories.py b/ietf/dbtemplate/factories.py index 92db4447bb..f33cc71132 100644 --- a/ietf/dbtemplate/factories.py +++ b/ietf/dbtemplate/factories.py @@ -2,7 +2,7 @@ from ietf.dbtemplate.models import DBTemplate + class DBTemplateFactory(factory.django.DjangoModelFactory): class Meta: model = DBTemplate - diff --git a/ietf/dbtemplate/forms.py b/ietf/dbtemplate/forms.py index 03f2bc0fcc..8420109b8c 100644 --- a/ietf/dbtemplate/forms.py +++ b/ietf/dbtemplate/forms.py @@ -9,25 +9,28 @@ from ietf.dbtemplate.models import DBTemplate from ietf.dbtemplate.template import PlainTemplate, RSTTemplate, DjangoTemplate -import debug # pyflakes:ignore +import debug # pyflakes:ignore + class DBTemplateForm(forms.ModelForm): def clean_content(self): try: - content = self.cleaned_data['content'] - if self.instance.type.slug == 'rst': + content = self.cleaned_data["content"] + if self.instance.type.slug == "rst": RSTTemplate(content).render(Context({})) - elif self.instance.type.slug == 'django': + elif self.instance.type.slug == "django": DjangoTemplate(content).render(Context({})) - elif self.instance.type.slug == 'plain': + elif self.instance.type.slug == "plain": PlainTemplate(content).render(Context({})) else: - raise ValidationError("Unexpected DBTemplate.type.slug: %s" % self.type.slug) + raise ValidationError( + "Unexpected DBTemplate.type.slug: %s" % self.type.slug + ) except Exception as e: raise ValidationError(e) return content - + class Meta: model = DBTemplate - fields = ('content', ) + fields = ("content",) diff --git a/ietf/dbtemplate/migrations/0001_initial.py b/ietf/dbtemplate/migrations/0001_initial.py index a3a23451f0..5fde459123 100644 --- a/ietf/dbtemplate/migrations/0001_initial.py +++ b/ietf/dbtemplate/migrations/0001_initial.py @@ -8,18 +8,25 @@ class Migration(migrations.Migration): initial = True - dependencies: List[Tuple[str, str]] = [ - ] + dependencies: List[Tuple[str, str]] = [] operations = [ migrations.CreateModel( - name='DBTemplate', + name="DBTemplate", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('path', models.CharField(max_length=255, unique=True)), - ('title', models.CharField(max_length=255)), - ('variables', models.TextField(blank=True, null=True)), - ('content', models.TextField()), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("path", models.CharField(max_length=255, unique=True)), + ("title", models.CharField(max_length=255)), + ("variables", models.TextField(blank=True, null=True)), + ("content", models.TextField()), ], ), ] diff --git a/ietf/dbtemplate/migrations/0002_auto_20230320_1222.py b/ietf/dbtemplate/migrations/0002_auto_20230320_1222.py index 5aa713635f..3348e11f57 100644 --- a/ietf/dbtemplate/migrations/0002_auto_20230320_1222.py +++ b/ietf/dbtemplate/migrations/0002_auto_20230320_1222.py @@ -10,20 +10,28 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('dbtemplate', '0001_initial'), - ('group', '0001_initial'), - ('name', '0001_initial'), + ("dbtemplate", "0001_initial"), + ("group", "0001_initial"), + ("name", "0001_initial"), ] operations = [ migrations.AddField( - model_name='dbtemplate', - name='group', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='group.Group'), + model_name="dbtemplate", + name="group", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="group.Group", + ), ), migrations.AddField( - model_name='dbtemplate', - name='type', - field=ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='name.DBTemplateTypeName'), + model_name="dbtemplate", + name="type", + field=ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="name.DBTemplateTypeName", + ), ), ] diff --git a/ietf/dbtemplate/models.py b/ietf/dbtemplate/models.py index 23900cfacd..c6276f90aa 100644 --- a/ietf/dbtemplate/models.py +++ b/ietf/dbtemplate/models.py @@ -12,34 +12,57 @@ TEMPLATE_TYPES = ( - ('plain', 'Plain'), - ('rst', 'reStructuredText'), - ('django', 'Django'), - ) + ("plain", "Plain"), + ("rst", "reStructuredText"), + ("django", "Django"), +) class DBTemplate(models.Model): - path = models.CharField( max_length=255, unique=True, blank=False, null=False, ) - title = models.CharField( max_length=255, blank=False, null=False, ) - variables = models.TextField( blank=True, null=True, ) - type = ForeignKey( DBTemplateTypeName, ) - content = models.TextField( blank=False, null=False, ) - group = ForeignKey( Group, blank=True, null=True, ) + path = models.CharField( + max_length=255, + unique=True, + blank=False, + null=False, + ) + title = models.CharField( + max_length=255, + blank=False, + null=False, + ) + variables = models.TextField( + blank=True, + null=True, + ) + type = ForeignKey( + DBTemplateTypeName, + ) + content = models.TextField( + blank=False, + null=False, + ) + group = ForeignKey( + Group, + blank=True, + null=True, + ) def __str__(self): return self.title def clean(self): from ietf.dbtemplate.template import PlainTemplate, RSTTemplate, DjangoTemplate + try: - if self.type.slug == 'rst': + if self.type.slug == "rst": RSTTemplate(self.content).render(Context({})) - elif self.type.slug == 'django': + elif self.type.slug == "django": DjangoTemplate(self.content).render(Context({})) - elif self.type.slug == 'plain': + elif self.type.slug == "plain": PlainTemplate(self.content).render(Context({})) else: - raise ValidationError("Unexpected DBTemplate.type.slug: %s" % self.type.slug) + raise ValidationError( + "Unexpected DBTemplate.type.slug: %s" % self.type.slug + ) except Exception as e: raise ValidationError(e) - diff --git a/ietf/dbtemplate/resources.py b/ietf/dbtemplate/resources.py index 3db6e4e118..011879e578 100644 --- a/ietf/dbtemplate/resources.py +++ b/ietf/dbtemplate/resources.py @@ -15,16 +15,21 @@ from ietf.group.resources import GroupResource from ietf.name.resources import DBTemplateTypeNameResource + + class DBTemplateResource(ModelResource): - type = ToOneField(DBTemplateTypeNameResource, 'type') - group = ToOneField(GroupResource, 'group', null=True) + type = ToOneField(DBTemplateTypeNameResource, "type") + group = ToOneField(GroupResource, "group", null=True) + class Meta: cache = SimpleCache() queryset = DBTemplate.objects.all() serializer = api.Serializer() - #resource_name = 'dbtemplate' - ordering = ['id', ] - filtering = { + # resource_name = 'dbtemplate' + ordering = [ + "id", + ] + filtering = { "id": ALL, "path": ALL, "title": ALL, @@ -33,5 +38,6 @@ class Meta: "type": ALL_WITH_RELATIONS, "group": ALL_WITH_RELATIONS, } -api.dbtemplate.register(DBTemplateResource()) + +api.dbtemplate.register(DBTemplateResource()) diff --git a/ietf/dbtemplate/template.py b/ietf/dbtemplate/template.py index 3491298e08..66abdef807 100644 --- a/ietf/dbtemplate/template.py +++ b/ietf/dbtemplate/template.py @@ -6,7 +6,7 @@ import string from docutils.core import publish_string from docutils.utils import SystemMessage -import debug # pyflakes:ignore +import debug # pyflakes:ignore from django.template import Origin, TemplateDoesNotExist, Template as DjangoTemplate from django.template.loaders.base import Loader as BaseLoader @@ -16,12 +16,14 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -RST_TEMPLATE = os.path.join(BASE_DIR, 'resources/rst.txt') +RST_TEMPLATE = os.path.join(BASE_DIR, "resources/rst.txt") class Template(DjangoTemplate): - def __init__(self, template_string, origin=None, name='', engine=None): + def __init__( + self, template_string, origin=None, name="", engine=None + ): super(Template, self).__init__(template_string, origin, name, engine) self.template_string = string.Template(template_string) @@ -43,22 +45,28 @@ class RSTTemplate(PlainTemplate): def render(self, context): interpolated_string = super(RSTTemplate, self).render(context) try: - return publish_string(source=interpolated_string, - writer_name='html', - settings_overrides={ - 'input_encoding': 'unicode', - 'output_encoding': 'unicode', - 'embed_stylesheet': False, - 'xml_declaration': False, - 'template': RST_TEMPLATE, - 'halt_level': 2, - }) + return publish_string( + source=interpolated_string, + writer_name="html", + settings_overrides={ + "input_encoding": "unicode", + "output_encoding": "unicode", + "embed_stylesheet": False, + "xml_declaration": False, + "template": RST_TEMPLATE, + "halt_level": 2, + }, + ) except SystemMessage as e: args = list(e.args) - args[0] = mark_safe('
%s
' % args[0].replace(':', 'line ')) + args[0] = mark_safe( + '
%s
' + % args[0].replace(":", "line ") + ) e.args = tuple(args) raise e + class Loader(BaseLoader): def __init__(self, engine): super(Loader, self).__init__(engine) @@ -75,22 +83,28 @@ def get_template(self, template_name, skip=None): for origin in self.get_template_sources(template_name): if skip is not None and origin in skip: - tried.append((origin, 'Skipped')) + tried.append((origin, "Skipped")) continue try: template = DBTemplate.objects.get(path=origin) contents = template.content except DBTemplate.DoesNotExist: - tried.append((origin, 'Source does not exist')) + tried.append((origin, "Source does not exist")) continue else: - if template.type_id == 'rst': - return RSTTemplate(contents, origin, origin.template_name, self.engine) - elif template.type_id == 'plain': - return PlainTemplate(contents, origin, origin.template_name, self.engine) - elif template.type_id == 'django': - return DjangoTemplate(contents, origin, origin.template_name, self.engine) + if template.type_id == "rst": + return RSTTemplate( + contents, origin, origin.template_name, self.engine + ) + elif template.type_id == "plain": + return PlainTemplate( + contents, origin, origin.template_name, self.engine + ) + elif template.type_id == "django": + return DjangoTemplate( + contents, origin, origin.template_name, self.engine + ) else: return Template(contents, origin, origin.template_name, self.engine) @@ -103,4 +117,3 @@ def get_template_sources(self, template_name): template_name=template_name, loader=self, ) - diff --git a/ietf/dbtemplate/urls.py b/ietf/dbtemplate/urls.py index 24e57da4eb..09a70bc655 100644 --- a/ietf/dbtemplate/urls.py +++ b/ietf/dbtemplate/urls.py @@ -1,9 +1,9 @@ - - from ietf.dbtemplate import views from ietf.utils.urls import url urlpatterns = [ - url(r'^(?P[-a-z0-9]+)/$', views.group_template_list), - url(r'^(?P[-a-z0-9]+)/(?P[\d]+)/$', views.group_template_edit), + url(r"^(?P[-a-z0-9]+)/$", views.group_template_list), + url( + r"^(?P[-a-z0-9]+)/(?P[\d]+)/$", views.group_template_edit + ), ] diff --git a/ietf/dbtemplate/views.py b/ietf/dbtemplate/views.py index 11b1a077e9..ce13154c39 100644 --- a/ietf/dbtemplate/views.py +++ b/ietf/dbtemplate/views.py @@ -3,7 +3,7 @@ from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, render -import debug # pyflakes:ignore +import debug # pyflakes:ignore from ietf.dbtemplate.models import DBTemplate from ietf.dbtemplate.forms import DBTemplateForm @@ -14,53 +14,79 @@ def group_template_list(request, acronym): group = get_object_or_404(Group, acronym=acronym) - chairs = group.role_set.filter(name__slug='chair') - if not has_role(request.user, "Secretariat") and not (request.user.id and chairs.filter(person__user=request.user).count()): + chairs = group.role_set.filter(name__slug="chair") + if not has_role(request.user, "Secretariat") and not ( + request.user.id and chairs.filter(person__user=request.user).count() + ): permission_denied(request, "You are not authorized to access this view.") template_list = DBTemplate.objects.filter(group=group) - return render(request, 'dbtemplate/template_list.html', - {'template_list': template_list, - 'group': group, - }) + return render( + request, + "dbtemplate/template_list.html", + { + "template_list": template_list, + "group": group, + }, + ) -def group_template_edit(request, acronym, template_id, base_template='dbtemplate/template_edit.html', formclass=DBTemplateForm, extra_context=None): +def group_template_edit( + request, + acronym, + template_id, + base_template="dbtemplate/template_edit.html", + formclass=DBTemplateForm, + extra_context=None, +): group = get_object_or_404(Group, acronym=acronym) - chairs = group.role_set.filter(name__slug='chair') + chairs = group.role_set.filter(name__slug="chair") extra_context = extra_context or {} - if not has_role(request.user, "Secretariat") and not (request.user.id and chairs.filter(person__user=request.user).count()): + if not has_role(request.user, "Secretariat") and not ( + request.user.id and chairs.filter(person__user=request.user).count() + ): permission_denied(request, "You are not authorized to access this view.") template = get_object_or_404(DBTemplate, id=template_id, group=group) - if request.method == 'POST': + if request.method == "POST": form = formclass(instance=template, data=request.POST) if form.is_valid(): form.save() - return HttpResponseRedirect('..') + return HttpResponseRedirect("..") else: form = formclass(instance=template) - context = {'template': template, - 'group': group, - 'form': form, + context = { + "template": template, + "group": group, + "form": form, } context.update(extra_context) return render(request, base_template, context) -def group_template_show(request, acronym, template_id, base_template='dbtemplate/template_edit.html', extra_context=None): + +def group_template_show( + request, + acronym, + template_id, + base_template="dbtemplate/template_edit.html", + extra_context=None, +): group = get_object_or_404(Group, acronym=acronym) - chairs = group.role_set.filter(name__slug='chair') + chairs = group.role_set.filter(name__slug="chair") extra_context = extra_context or {} - if not has_role(request.user, "Secretariat") and not (request.user.id and chairs.filter(person__user=request.user).count()): + if not has_role(request.user, "Secretariat") and not ( + request.user.id and chairs.filter(person__user=request.user).count() + ): permission_denied(request, "You are not authorized to access this view.") template = get_object_or_404(DBTemplate, id=template_id, group=group) - context = {'template': template, - 'group': group, + context = { + "template": template, + "group": group, } context.update(extra_context) return render(request, base_template, context) diff --git a/ietf/doc/admin.py b/ietf/doc/admin.py index db3b24b2d2..d1c8096256 100644 --- a/ietf/doc/admin.py +++ b/ietf/doc/admin.py @@ -6,160 +6,315 @@ from django.db import models from django import forms -from .models import (StateType, State, RelatedDocument, DocumentAuthor, Document, RelatedDocHistory, - DocHistoryAuthor, DocHistory, DocReminder, DocEvent, NewRevisionDocEvent, - StateDocEvent, ConsensusDocEvent, BallotType, BallotDocEvent, WriteupDocEvent, LastCallDocEvent, - TelechatDocEvent, BallotPositionDocEvent, ReviewRequestDocEvent, InitialReviewDocEvent, - AddedMessageEvent, SubmissionDocEvent, DeletedEvent, EditedAuthorsDocEvent, DocumentURL, - ReviewAssignmentDocEvent, IanaExpertDocEvent, IRSGBallotDocEvent, DocExtResource, DocumentActionHolder, - BofreqEditorDocEvent, BofreqResponsibleDocEvent, StoredObject ) +from .models import ( + StateType, + State, + RelatedDocument, + DocumentAuthor, + Document, + RelatedDocHistory, + DocHistoryAuthor, + DocHistory, + DocReminder, + DocEvent, + NewRevisionDocEvent, + StateDocEvent, + ConsensusDocEvent, + BallotType, + BallotDocEvent, + WriteupDocEvent, + LastCallDocEvent, + TelechatDocEvent, + BallotPositionDocEvent, + ReviewRequestDocEvent, + InitialReviewDocEvent, + AddedMessageEvent, + SubmissionDocEvent, + DeletedEvent, + EditedAuthorsDocEvent, + DocumentURL, + ReviewAssignmentDocEvent, + IanaExpertDocEvent, + IRSGBallotDocEvent, + DocExtResource, + DocumentActionHolder, + BofreqEditorDocEvent, + BofreqResponsibleDocEvent, + StoredObject, +) from ietf.utils.validators import validate_external_resource_value + class StateTypeAdmin(admin.ModelAdmin): list_display = ["slug", "label"] + + admin.site.register(StateType, StateTypeAdmin) + class StateAdmin(admin.ModelAdmin): - list_display = ["slug", "type", 'name', 'order', 'desc'] - list_filter = ["type", ] + list_display = ["slug", "type", "name", "order", "desc"] + list_filter = [ + "type", + ] search_fields = ["slug", "type__label", "type__slug", "name", "desc"] filter_horizontal = ["next_states"] + + admin.site.register(State, StateAdmin) + class DocAuthorInline(admin.TabularInline): model = DocumentAuthor - raw_id_fields = ['person', 'email'] + raw_id_fields = ["person", "email"] extra = 1 + class DocActionHolderInline(admin.TabularInline): model = DocumentActionHolder - raw_id_fields = ['person'] + raw_id_fields = ["person"] extra = 1 + class RelatedDocumentInline(admin.TabularInline): model = RelatedDocument - fk_name= 'source' + fk_name = "source" + def this(self, instance): return instance.source.name - readonly_fields = ['this', ] - fields = ['this', 'relationship', 'target', ] - raw_id_fields = ['target'] + + readonly_fields = [ + "this", + ] + fields = [ + "this", + "relationship", + "target", + ] + raw_id_fields = ["target"] extra = 1 + class AdditionalUrlInLine(admin.TabularInline): model = DocumentURL - fields = ['tag','desc','url',] + fields = [ + "tag", + "desc", + "url", + ] extra = 1 formfield_overrides = { - models.CharField: {'widget': forms.TextInput(attrs={'size':'50'})}, + models.CharField: {"widget": forms.TextInput(attrs={"size": "50"})}, } + class DocumentForm(forms.ModelForm): comment_about_changes = forms.CharField( - widget=forms.Textarea(attrs={'rows':10,'cols':40,'class':'vLargeTextField'}), strip=False, - help_text="This comment about the changes made will be saved in the document history.") - + widget=forms.Textarea( + attrs={"rows": 10, "cols": 40, "class": "vLargeTextField"} + ), + strip=False, + help_text="This comment about the changes made will be saved in the document history.", + ) + class Meta: - fields = '__all__' - exclude = ('states',) + fields = "__all__" + exclude = ("states",) model = Document + class DocumentAuthorAdmin(admin.ModelAdmin): - list_display = ['id', 'document', 'person', 'email', 'affiliation', 'country', 'order'] - search_fields = ['document__name', 'person__name', 'email__address', 'affiliation', 'country'] + list_display = [ + "id", + "document", + "person", + "email", + "affiliation", + "country", + "order", + ] + search_fields = [ + "document__name", + "person__name", + "email__address", + "affiliation", + "country", + ] raw_id_fields = ["document", "person", "email"] + + admin.site.register(DocumentAuthor, DocumentAuthorAdmin) - + + class DocumentAdmin(admin.ModelAdmin): - list_display = ['name', 'rev', 'group', 'pages', 'intended_std_level', 'author_list', 'time'] - search_fields = ['name'] - list_filter = ['type'] - raw_id_fields = ['group', 'shepherd', 'ad'] - inlines = [DocAuthorInline, DocActionHolderInline, RelatedDocumentInline, AdditionalUrlInLine] + list_display = [ + "name", + "rev", + "group", + "pages", + "intended_std_level", + "author_list", + "time", + ] + search_fields = ["name"] + list_filter = ["type"] + raw_id_fields = ["group", "shepherd", "ad"] + inlines = [ + DocAuthorInline, + DocActionHolderInline, + RelatedDocumentInline, + AdditionalUrlInLine, + ] form = DocumentForm def save_model(self, request, obj, form, change): e = DocEvent.objects.create( - doc=obj, - rev=obj.rev, - by=request.user.person, - type='changed_document', - desc=form.cleaned_data.get('comment_about_changes'), - ) + doc=obj, + rev=obj.rev, + by=request.user.person, + type="changed_document", + desc=form.cleaned_data.get("comment_about_changes"), + ) obj.save_with_history([e]) def state(self, instance): return self.get_state() + admin.site.register(Document, DocumentAdmin) + class DocHistoryAdmin(admin.ModelAdmin): - list_display = ['doc', 'rev', 'state', 'group', 'pages', 'intended_std_level', 'author_list', 'time'] - search_fields = ['doc__name'] - ordering = ['time', 'doc', 'rev'] - raw_id_fields = ['doc', 'group', 'shepherd', 'ad'] + list_display = [ + "doc", + "rev", + "state", + "group", + "pages", + "intended_std_level", + "author_list", + "time", + ] + search_fields = ["doc__name"] + ordering = ["time", "doc", "rev"] + raw_id_fields = ["doc", "group", "shepherd", "ad"] def state(self, instance): return instance.get_state() + admin.site.register(DocHistory, DocHistoryAdmin) + class DocReminderAdmin(admin.ModelAdmin): - list_display = ['id', 'event', 'type', 'due', 'active'] - list_filter = ['type', 'due', 'active'] - raw_id_fields = ['event'] + list_display = ["id", "event", "type", "due", "active"] + list_filter = ["type", "due", "active"] + raw_id_fields = ["event"] + + admin.site.register(DocReminder, DocReminderAdmin) + class RelatedDocumentAdmin(admin.ModelAdmin): - list_display = ['source', 'target', 'relationship', ] - list_filter = ['relationship', ] - search_fields = ['source__name', 'target__name', ] - raw_id_fields = ['source', 'target', ] + list_display = [ + "source", + "target", + "relationship", + ] + list_filter = [ + "relationship", + ] + search_fields = [ + "source__name", + "target__name", + ] + raw_id_fields = [ + "source", + "target", + ] + + admin.site.register(RelatedDocument, RelatedDocumentAdmin) + class RelatedDocHistoryAdmin(admin.ModelAdmin): - list_display = ['id', 'source', 'target', 'relationship'] - list_filter = ['relationship'] - raw_id_fields = ['source', 'target'] + list_display = ["id", "source", "target", "relationship"] + list_filter = ["relationship"] + raw_id_fields = ["source", "target"] + + admin.site.register(RelatedDocHistory, RelatedDocHistoryAdmin) + class DocHistoryAuthorAdmin(admin.ModelAdmin): - list_display = ['id', 'person', 'email', 'affiliation', 'country', 'order', 'document'] - raw_id_fields = ['person', 'email', 'document'] + list_display = [ + "id", + "person", + "email", + "affiliation", + "country", + "order", + "document", + ] + raw_id_fields = ["person", "email", "document"] + + admin.site.register(DocHistoryAuthor, DocHistoryAuthorAdmin) + class BallotTypeAdmin(admin.ModelAdmin): list_display = ["slug", "doc_type", "name", "question"] + + admin.site.register(BallotType, BallotTypeAdmin) class DocumentActionHolderAdmin(admin.ModelAdmin): - list_display = ['id', 'document', 'person', 'time_added'] - raw_id_fields = ['document', 'person'] + list_display = ["id", "document", "person", "time_added"] + raw_id_fields = ["document", "person"] + + admin.site.register(DocumentActionHolder, DocumentActionHolderAdmin) # events + class DeletedEventAdmin(admin.ModelAdmin): - list_display = ['id', 'content_type', 'json', 'by', 'time'] - list_filter = ['time'] - raw_id_fields = ['content_type', 'by'] + list_display = ["id", "content_type", "json", "by", "time"] + list_filter = ["time"] + raw_id_fields = ["content_type", "by"] + + admin.site.register(DeletedEvent, DeletedEventAdmin) class DocEventAdmin(admin.ModelAdmin): def event_type(self, obj): return str(obj.type) + def doc_time(self, obj): h = obj.get_dochistory() return h.time if h else "" + def short_desc(self, obj): return obj.desc[:32] - list_display = ["id", "doc", "event_type", "rev", "by", "time", "doc_time", "short_desc" ] + + list_display = [ + "id", + "doc", + "event_type", + "rev", + "by", + "time", + "doc_time", + "short_desc", + ] search_fields = ["doc__name", "by__name"] raw_id_fields = ["doc", "by"] + + admin.site.register(DocEvent, DocEventAdmin) admin.site.register(NewRevisionDocEvent, DocEventAdmin) @@ -174,53 +329,110 @@ def short_desc(self, obj): admin.site.register(EditedAuthorsDocEvent, DocEventAdmin) admin.site.register(IanaExpertDocEvent, DocEventAdmin) + class BallotPositionDocEventAdmin(DocEventAdmin): raw_id_fields = DocEventAdmin.raw_id_fields + ["balloter", "ballot"] + + admin.site.register(BallotPositionDocEvent, BallotPositionDocEventAdmin) + class BofreqEditorDocEventAdmin(DocEventAdmin): raw_id_fields = DocEventAdmin.raw_id_fields + ["editors"] + + admin.site.register(BofreqEditorDocEvent, BofreqEditorDocEventAdmin) - + + class BofreqResponsibleDocEventAdmin(DocEventAdmin): raw_id_fields = DocEventAdmin.raw_id_fields + ["responsible"] + + admin.site.register(BofreqResponsibleDocEvent, BofreqResponsibleDocEventAdmin) - + + class ReviewRequestDocEventAdmin(DocEventAdmin): raw_id_fields = DocEventAdmin.raw_id_fields + ["review_request"] + + admin.site.register(ReviewRequestDocEvent, ReviewRequestDocEventAdmin) + class ReviewAssignmentDocEventAdmin(DocEventAdmin): raw_id_fields = DocEventAdmin.raw_id_fields + ["review_assignment"] + + admin.site.register(ReviewAssignmentDocEvent, ReviewAssignmentDocEventAdmin) + class AddedMessageEventAdmin(DocEventAdmin): raw_id_fields = DocEventAdmin.raw_id_fields + ["message"] + + admin.site.register(AddedMessageEvent, AddedMessageEventAdmin) + class SubmissionDocEventAdmin(DocEventAdmin): raw_id_fields = DocEventAdmin.raw_id_fields + ["submission"] + + admin.site.register(SubmissionDocEvent, SubmissionDocEventAdmin) + class DocumentUrlAdmin(admin.ModelAdmin): - list_display = ['id', 'doc', 'tag', 'url', 'desc', ] - search_fields = ['doc__name', 'url', ] - raw_id_fields = ['doc', ] + list_display = [ + "id", + "doc", + "tag", + "url", + "desc", + ] + search_fields = [ + "doc__name", + "url", + ] + raw_id_fields = [ + "doc", + ] + + admin.site.register(DocumentURL, DocumentUrlAdmin) + class DocExtResourceAdminForm(forms.ModelForm): def clean(self): - validate_external_resource_value(self.cleaned_data['name'],self.cleaned_data['value']) + validate_external_resource_value( + self.cleaned_data["name"], self.cleaned_data["value"] + ) + class DocExtResourceAdmin(admin.ModelAdmin): form = DocExtResourceAdminForm - list_display = ['id', 'doc', 'name', 'display_name', 'value',] - search_fields = ['doc__name', 'value', 'display_name', 'name__slug',] - raw_id_fields = ['doc', ] + list_display = [ + "id", + "doc", + "name", + "display_name", + "value", + ] + search_fields = [ + "doc__name", + "value", + "display_name", + "name__slug", + ] + raw_id_fields = [ + "doc", + ] + + admin.site.register(DocExtResource, DocExtResourceAdmin) + class StoredObjectAdmin(admin.ModelAdmin): - list_display = ['store', 'name', 'modified', 'deleted'] - list_filter = ['deleted'] - search_fields = ['store', 'name', 'doc_name', 'doc_rev', 'deleted'] + list_display = ["store", "name", "modified", "deleted"] + list_filter = ["deleted"] + search_fields = ["store", "name", "doc_name", "doc_rev", "deleted"] + + admin.site.register(StoredObject, StoredObjectAdmin) diff --git a/ietf/doc/expire.py b/ietf/doc/expire.py index bf8523aa98..7ec2644a92 100644 --- a/ietf/doc/expire.py +++ b/ietf/doc/expire.py @@ -3,7 +3,7 @@ # expiry of Internet-Drafts -import debug # pyflakes:ignore +import debug # pyflakes:ignore from django.conf import settings from django.utils import timezone @@ -11,14 +11,14 @@ import datetime, os, shutil, glob, re from pathlib import Path -from typing import List, Optional # pyflakes:ignore +from typing import List, Optional # pyflakes:ignore from ietf.doc.storage_utils import exists_in_storage, remove_from_storage from ietf.doc.utils import update_action_holders from ietf.utils import log from ietf.utils.mail import send_mail from ietf.doc.models import Document, DocEvent, State -from ietf.person.models import Person +from ietf.person.models import Person from ietf.meeting.models import Meeting from ietf.mailtrigger.utils import gather_address_lists from ietf.utils.timezone import date_today, datetime_today, DEADLINE_TZINFO @@ -26,6 +26,7 @@ nonexpirable_states: Optional[List[State]] = None + def expirable_drafts(queryset=None): """Return a queryset with expirable drafts.""" global nonexpirable_states @@ -38,22 +39,39 @@ def expirable_drafts(queryset=None): # Populate this first time through (but after django has been set up) if nonexpirable_states is None: # all IESG states except I-D Exists and Dead block expiry - nonexpirable_states = list(State.objects.filter(used=True, type="draft-iesg").exclude(slug__in=("idexists", "dead"))) + nonexpirable_states = list( + State.objects.filter(used=True, type="draft-iesg").exclude( + slug__in=("idexists", "dead") + ) + ) # sent to RFC Editor and RFC Published block expiry (the latter # shouldn't be possible for an active draft, though) - nonexpirable_states += list(State.objects.filter(used=True, type__in=("draft-stream-iab", "draft-stream-irtf", "draft-stream-ise"), slug__in=("rfc-edit", "pub"))) + nonexpirable_states += list( + State.objects.filter( + used=True, + type__in=("draft-stream-iab", "draft-stream-irtf", "draft-stream-ise"), + slug__in=("rfc-edit", "pub"), + ) + ) # other IRTF states that block expiration - nonexpirable_states += list(State.objects.filter(used=True, type_id="draft-stream-irtf", slug__in=("irsgpoll", "iesg-rev",))) - - return queryset.filter( - states__type="draft", states__slug="active" - ).exclude( - expires=None - ).exclude( - states__in=nonexpirable_states - ).exclude( - tags="rfc-rev" # under review by the RFC Editor blocks expiry - ).distinct() + nonexpirable_states += list( + State.objects.filter( + used=True, + type_id="draft-stream-irtf", + slug__in=( + "irsgpoll", + "iesg-rev", + ), + ) + ) + + return ( + queryset.filter(states__type="draft", states__slug="active") + .exclude(expires=None) + .exclude(states__in=nonexpirable_states) + .exclude(tags="rfc-rev") # under review by the RFC Editor blocks expiry + .distinct() + ) def get_soon_to_expire_drafts(days_of_warning): @@ -62,14 +80,22 @@ def get_soon_to_expire_drafts(days_of_warning): return expirable_drafts().filter(expires__gte=start_date, expires__lt=end_date) + def get_expired_drafts(): - return expirable_drafts().filter(expires__lt=datetime_today(DEADLINE_TZINFO) + datetime.timedelta(1)) + return expirable_drafts().filter( + expires__lt=datetime_today(DEADLINE_TZINFO) + datetime.timedelta(1) + ) + def in_draft_expire_freeze(when=None): if when == None: when = timezone.now() - meeting = Meeting.objects.filter(type='ietf', date__gte=when-datetime.timedelta(days=7)).order_by('date').first() + meeting = ( + Meeting.objects.filter(type="ietf", date__gte=when - datetime.timedelta(days=7)) + .order_by("date") + .first() + ) if not meeting: return False @@ -83,11 +109,13 @@ def in_draft_expire_freeze(when=None): return second_cut_off <= when < ietf_monday + def send_expire_warning_for_draft(doc): - if ((doc.get_state_slug("draft-iesg") == "dead") or - (doc.get_state_slug("draft") != "active")): - return # don't warn about dead or inactive documents + if (doc.get_state_slug("draft-iesg") == "dead") or ( + doc.get_state_slug("draft") != "active" + ): + return # don't warn about dead or inactive documents expiration = doc.expires.astimezone(DEADLINE_TZINFO).date() now_plus_12hours = timezone.now() + datetime.timedelta(hours=12) @@ -97,43 +125,53 @@ def send_expire_warning_for_draft(doc): # same people, so do not send the warning at this point in time return - - (to,cc) = gather_address_lists('doc_expires_soon',doc=doc) + (to, cc) = gather_address_lists("doc_expires_soon", doc=doc) s = doc.get_state("draft-iesg") - log.assertion('s') - state = s.name if s else "I-D Exists" # TODO remove the if clause after some runtime shows no assertions + log.assertion("s") + state = ( + s.name if s else "I-D Exists" + ) # TODO remove the if clause after some runtime shows no assertions frm = None request = None if to or cc: - send_mail(request, to, frm, - "Expiration impending: %s" % doc.file_tag(), - "doc/draft/expire_warning_email.txt", - dict(doc=doc, - state=state, - expiration=expiration - ), - cc=cc) + send_mail( + request, + to, + frm, + "Expiration impending: %s" % doc.file_tag(), + "doc/draft/expire_warning_email.txt", + dict(doc=doc, state=state, expiration=expiration), + cc=cc, + ) + def send_expire_notice_for_draft(doc): if doc.get_state_slug("draft-iesg") == "dead": return s = doc.get_state("draft-iesg") - log.assertion('s') - state = s.name if s else "I-D Exists" # TODO remove the if clause after some rintime shows no assertions + log.assertion("s") + state = ( + s.name if s else "I-D Exists" + ) # TODO remove the if clause after some rintime shows no assertions request = None - (to,cc) = gather_address_lists('doc_expired',doc=doc) - send_mail(request, to, - "I-D Expiring System ", - "I-D was expired %s" % doc.file_tag(), - "doc/draft/id_expired_email.txt", - dict(doc=doc, - state=state, - ), - cc=cc) + (to, cc) = gather_address_lists("doc_expired", doc=doc) + send_mail( + request, + to, + "I-D Expiring System ", + "I-D was expired %s" % doc.file_tag(), + "doc/draft/id_expired_email.txt", + dict( + doc=doc, + state=state, + ), + cc=cc, + ) + def move_draft_files_to_archive(doc, rev): def move_file(f): @@ -151,7 +189,7 @@ def move_file(f): pass else: raise - + def remove_ftp_copy(f): mark = Path(settings.FTP_DIR) / "internet-drafts" / f if mark.exists(): @@ -169,6 +207,7 @@ def remove_from_active_draft_storage(file): remove_ftp_copy(str(file.name)) remove_from_active_draft_storage(file) + def expire_draft(doc): # clean up files move_draft_files_to_archive(doc, doc.rev) @@ -177,19 +216,30 @@ def expire_draft(doc): events = [] - events.append(DocEvent.objects.create(doc=doc, rev=doc.rev, by=system, type="expired_document", desc="Document has expired")) - - prev_draft_state=doc.get_state("draft") + events.append( + DocEvent.objects.create( + doc=doc, + rev=doc.rev, + by=system, + type="expired_document", + desc="Document has expired", + ) + ) + + prev_draft_state = doc.get_state("draft") doc.set_state(State.objects.get(used=True, type="draft", slug="expired")) - events.append(update_action_holders(doc, prev_draft_state, doc.get_state("draft"),[],[])) + events.append( + update_action_holders(doc, prev_draft_state, doc.get_state("draft"), [], []) + ) doc.save_with_history(events) + def clean_up_draft_files(): """Move unidentified and old files out of the Internet-Draft directory.""" cut_off = date_today(DEADLINE_TZINFO) pattern = os.path.join(settings.INTERNET_DRAFT_PATH, "draft-*.*") - filename_re = re.compile(r'^(.*)-(\d\d)$') + filename_re = re.compile(r"^(.*)-(\d\d)$") def splitext(fn): """ @@ -205,9 +255,9 @@ def splitext(fn): s = 0 i = fn[s:].find(".") if i == -1: - return fn, '' + return fn, "" else: - return fn[:s+i], fn[s+i:] + return fn[: s + i], fn[s + i :] for path in glob.glob(pattern): basename = os.path.basename(path) @@ -220,8 +270,10 @@ def splitext(fn): def move_file_to(subdir): # Similar to move_draft_files_to_archive - shutil.move(path, - os.path.join(settings.INTERNET_DRAFT_ARCHIVE_DIR, subdir, basename)) + shutil.move( + path, + os.path.join(settings.INTERNET_DRAFT_ARCHIVE_DIR, subdir, basename), + ) mark = Path(settings.FTP_DIR) / "internet-drafts" / basename if mark.exists(): mark.unlink() @@ -238,14 +290,15 @@ def move_file_to(subdir): state = doc.get_state_slug() - if state in ("rfc","repl"): + if state in ("rfc", "repl"): move_file_to("") - elif (state in ("expired", "auth-rm", "ietf-rm") - and doc.expires - and doc.expires.astimezone(DEADLINE_TZINFO).date() < cut_off): + elif ( + state in ("expired", "auth-rm", "ietf-rm") + and doc.expires + and doc.expires.astimezone(DEADLINE_TZINFO).date() < cut_off + ): move_file_to("") except Document.DoesNotExist: # All uses of this past 2014 seem related to major system failures. move_file_to("unknown_ids") - diff --git a/ietf/doc/factories.py b/ietf/doc/factories.py index 19aa9ecc9c..9d401cead6 100644 --- a/ietf/doc/factories.py +++ b/ietf/doc/factories.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- -import debug # pyflakes:ignore +import debug # pyflakes:ignore import factory import factory.fuzzy import datetime @@ -12,9 +12,23 @@ from django.conf import settings from django.utils import timezone -from ietf.doc.models import ( Document, DocEvent, NewRevisionDocEvent, State, DocumentAuthor, - StateDocEvent, BallotPositionDocEvent, BallotDocEvent, BallotType, IRSGBallotDocEvent, TelechatDocEvent, - DocumentActionHolder, BofreqEditorDocEvent, BofreqResponsibleDocEvent, DocExtResource ) +from ietf.doc.models import ( + Document, + DocEvent, + NewRevisionDocEvent, + State, + DocumentAuthor, + StateDocEvent, + BallotPositionDocEvent, + BallotDocEvent, + BallotType, + IRSGBallotDocEvent, + TelechatDocEvent, + DocumentActionHolder, + BofreqEditorDocEvent, + BofreqResponsibleDocEvent, + DocExtResource, +) from ietf.group.models import Group from ietf.person.factories import PersonFactory from ietf.group.factories import RoleFactory @@ -23,14 +37,15 @@ from ietf.utils.timezone import date_today -def draft_name_generator(type_id,group,n): - return '%s-%s-%s-%s%d'%( - type_id, - 'bogusperson', - group.acronym if group else 'netherwhere', - 'musings', - n, - ) +def draft_name_generator(type_id, group, n): + return "%s-%s-%s-%s%d" % ( + type_id, + "bogusperson", + group.acronym if group else "netherwhere", + "musings", + n, + ) + class BaseDocumentFactory(factory.django.DjangoModelFactory): class Meta: @@ -38,53 +53,61 @@ class Meta: skip_postgeneration_save = True # n.b., a few attributes are typed as Any so mypy won't complain when we override in subclasses - title = factory.Faker('sentence',nb_words=5) - abstract: Any = factory.Faker('paragraph', nb_sentences=5) - rev = '00' + title = factory.Faker("sentence", nb_words=5) + abstract: Any = factory.Faker("paragraph", nb_sentences=5) + rev = "00" std_level_id: Any = None intended_std_level_id = None time = timezone.now() expires: Any = factory.LazyAttribute( - lambda o: o.time+datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE) + lambda o: o.time + + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE) ) - pages = factory.fuzzy.FuzzyInteger(2,400) - + pages = factory.fuzzy.FuzzyInteger(2, 400) @factory.lazy_attribute_sequence def name(self, n): - return draft_name_generator(self.type_id,self.group,n) + return draft_name_generator(self.type_id, self.group, n) @factory.post_generation - def newrevisiondocevent(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument + def newrevisiondocevent( + obj, create, extracted, **kwargs + ): # pylint: disable=no-self-argument if create: if obj.type_id != "rfc": NewRevisionDocEventFactory(doc=obj) @factory.post_generation - def states(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument + def states(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument if create and extracted: - for (state_type_id,state_slug) in extracted: - obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug)) - if obj.type_id == 'draft': - if not obj.states.filter(type_id='draft-iesg').exists(): - obj.set_state(State.objects.get(type_id='draft-iesg', slug='idexists')) + for state_type_id, state_slug in extracted: + obj.set_state(State.objects.get(type_id=state_type_id, slug=state_slug)) + if obj.type_id == "draft": + if not obj.states.filter(type_id="draft-iesg").exists(): + obj.set_state( + State.objects.get(type_id="draft-iesg", slug="idexists") + ) @factory.post_generation - def authors(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument + def authors(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument if create and extracted: order = 0 for person in extracted: - DocumentAuthor.objects.create(document=obj, person=person, email=person.email(), order=order) + DocumentAuthor.objects.create( + document=obj, person=person, email=person.email(), order=order + ) order += 1 @factory.post_generation - def relations(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument + def relations(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument if create and extracted: - for (rel_id, doc) in extracted: + for rel_id, doc in extracted: obj.relateddocument_set.create(relationship_id=rel_id, target=doc) @factory.post_generation - def create_revisions(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument + def create_revisions( + obj, create, extracted, **kwargs + ): # pylint: disable=no-self-argument """Create additional revisions of the document Argument should be an iterable of revisions. Remember that range() is exclusive on the end @@ -92,8 +115,8 @@ def create_revisions(obj, create, extracted, **kwargs): # pylint: disable=no-se """ if create and extracted: for rev in extracted: - e = NewRevisionDocEventFactory(doc=obj, rev=f'{rev:02d}') - obj.rev = f'{rev:02d}' + e = NewRevisionDocEventFactory(doc=obj, rev=f"{rev:02d}") + obj.rev = f"{rev:02d}" obj.save_with_history([e]) @classmethod @@ -104,11 +127,12 @@ def _after_postgeneration(cls, obj, create, results=None): obj._has_an_event_so_saving_is_allowed = True obj.save() -#TODO remove this - rename BaseDocumentFactory to DocumentFactory + +# TODO remove this - rename BaseDocumentFactory to DocumentFactory class DocumentFactory(BaseDocumentFactory): - type_id = 'draft' - group = factory.SubFactory('ietf.group.factories.GroupFactory',acronym='none') + type_id = "draft" + group = factory.SubFactory("ietf.group.factories.GroupFactory", acronym="none") class RfcFactory(BaseDocumentFactory): @@ -123,92 +147,93 @@ def states(obj, create, extracted, **kwargs): if not create: return if extracted: - for (state_type_id,state_slug) in extracted: - obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug)) + for state_type_id, state_slug in extracted: + obj.set_state(State.objects.get(type_id=state_type_id, slug=state_slug)) else: - obj.set_state(State.objects.get(type_id='rfc',slug='published')) + obj.set_state(State.objects.get(type_id="rfc", slug="published")) class IndividualDraftFactory(BaseDocumentFactory): - type_id = 'draft' - group = factory.SubFactory('ietf.group.factories.GroupFactory',acronym='none') + type_id = "draft" + group = factory.SubFactory("ietf.group.factories.GroupFactory", acronym="none") @factory.post_generation def states(obj, create, extracted, **kwargs): if not create: return if extracted: - for (state_type_id,state_slug) in extracted: - obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug)) - if not obj.get_state('draft-iesg'): - obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists')) + for state_type_id, state_slug in extracted: + obj.set_state(State.objects.get(type_id=state_type_id, slug=state_slug)) + if not obj.get_state("draft-iesg"): + obj.set_state(State.objects.get(type_id="draft-iesg", slug="idexists")) else: - obj.set_state(State.objects.get(type_id='draft',slug='active')) - obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists')) + obj.set_state(State.objects.get(type_id="draft", slug="active")) + obj.set_state(State.objects.get(type_id="draft-iesg", slug="idexists")) + class IndividualRfcFactory(RfcFactory): - group = factory.SubFactory('ietf.group.factories.GroupFactory',acronym='none') + group = factory.SubFactory("ietf.group.factories.GroupFactory", acronym="none") class WgDraftFactory(BaseDocumentFactory): - type_id = 'draft' - group = factory.SubFactory('ietf.group.factories.GroupFactory',type_id='wg') - stream_id = 'ietf' + type_id = "draft" + group = factory.SubFactory("ietf.group.factories.GroupFactory", type_id="wg") + stream_id = "ietf" @factory.post_generation def states(obj, create, extracted, **kwargs): if not create: return if extracted: - for (state_type_id,state_slug) in extracted: - obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug)) - if not obj.get_state('draft-iesg'): - obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists')) + for state_type_id, state_slug in extracted: + obj.set_state(State.objects.get(type_id=state_type_id, slug=state_slug)) + if not obj.get_state("draft-iesg"): + obj.set_state(State.objects.get(type_id="draft-iesg", slug="idexists")) else: - obj.set_state(State.objects.get(type_id='draft',slug='active')) - obj.set_state(State.objects.get(type_id='draft-stream-ietf',slug='wg-doc')) - obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists')) + obj.set_state(State.objects.get(type_id="draft", slug="active")) + obj.set_state(State.objects.get(type_id="draft-stream-ietf", slug="wg-doc")) + obj.set_state(State.objects.get(type_id="draft-iesg", slug="idexists")) class WgRfcFactory(RfcFactory): - group = factory.SubFactory('ietf.group.factories.GroupFactory',type_id='wg') - stream_id = 'ietf' - std_level_id = 'ps' + group = factory.SubFactory("ietf.group.factories.GroupFactory", type_id="wg") + stream_id = "ietf" + std_level_id = "ps" class RgDraftFactory(BaseDocumentFactory): - type_id = 'draft' - group = factory.SubFactory('ietf.group.factories.GroupFactory',type_id='rg') - stream_id = 'irtf' + type_id = "draft" + group = factory.SubFactory("ietf.group.factories.GroupFactory", type_id="rg") + stream_id = "irtf" @factory.post_generation def states(obj, create, extracted, **kwargs): if not create: return if extracted: - for (state_type_id,state_slug) in extracted: - obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug)) - if not obj.get_state('draft-iesg'): - obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists')) + for state_type_id, state_slug in extracted: + obj.set_state(State.objects.get(type_id=state_type_id, slug=state_slug)) + if not obj.get_state("draft-iesg"): + obj.set_state(State.objects.get(type_id="draft-iesg", slug="idexists")) else: - obj.set_state(State.objects.get(type_id='draft',slug='active')) - obj.set_state(State.objects.get(type_id='draft-stream-irtf',slug='active')) - obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists')) + obj.set_state(State.objects.get(type_id="draft", slug="active")) + obj.set_state(State.objects.get(type_id="draft-stream-irtf", slug="active")) + obj.set_state(State.objects.get(type_id="draft-iesg", slug="idexists")) class RgRfcFactory(RfcFactory): - group = factory.SubFactory('ietf.group.factories.GroupFactory',type_id='rg') - stream_id = 'irtf' - std_level_id = 'inf' + group = factory.SubFactory("ietf.group.factories.GroupFactory", type_id="rg") + stream_id = "irtf" + std_level_id = "inf" class CharterFactory(BaseDocumentFactory): - type_id = 'charter' - group = factory.SubFactory('ietf.group.factories.GroupFactory',type_id='wg') - name = factory.LazyAttribute(lambda o: 'charter-ietf-%s'%o.group.acronym) + type_id = "charter" + group = factory.SubFactory("ietf.group.factories.GroupFactory", type_id="wg") + name = factory.LazyAttribute(lambda o: "charter-ietf-%s" % o.group.acronym) @factory.post_generation def set_group_charter_document(obj, create, extracted, **kwargs): @@ -217,21 +242,26 @@ def set_group_charter_document(obj, create, extracted, **kwargs): obj.group.charter = extracted or obj obj.group.save() + class StatusChangeFactory(BaseDocumentFactory): - type_id='statchg' + type_id = "statchg" - group = factory.SubFactory('ietf.group.factories.GroupFactory',acronym='iesg',type_id='ietf') - name = factory.Sequence(lambda n: f'status-change-{n}-factoried') + group = factory.SubFactory( + "ietf.group.factories.GroupFactory", acronym="iesg", type_id="ietf" + ) + name = factory.Sequence(lambda n: f"status-change-{n}-factoried") @factory.post_generation def changes_status_of(obj, create, extracted, **kwargs): if not create: return if extracted: - for (rel, target) in extracted: - obj.relateddocument_set.create(relationship_id=rel,target=target) + for rel, target in extracted: + obj.relateddocument_set.create(relationship_id=rel, target=target) else: - obj.relateddocument_set.create(relationship_id='tobcp', target=WgRfcFactory()) + obj.relateddocument_set.create( + relationship_id="tobcp", target=WgRfcFactory() + ) @factory.post_generation def states(obj, create, extracted, **kwargs): @@ -241,27 +271,35 @@ def states(obj, create, extracted, **kwargs): for state in extracted: obj.set_state(state) else: - obj.set_state(State.objects.get(type_id='statchg',slug='appr-sent')) + obj.set_state(State.objects.get(type_id="statchg", slug="appr-sent")) class ConflictReviewFactory(BaseDocumentFactory): - type_id='conflrev' + type_id = "conflrev" - group = factory.SubFactory('ietf.group.factories.GroupFactory',acronym='none') + group = factory.SubFactory("ietf.group.factories.GroupFactory", acronym="none") @factory.lazy_attribute_sequence def name(self, n): - return draft_name_generator(self.type_id,self.group,n).replace('conflrev-','conflict-review-') - + return draft_name_generator(self.type_id, self.group, n).replace( + "conflrev-", "conflict-review-" + ) + @factory.post_generation def review_of(obj, create, extracted, **kwargs): if not create: return if extracted: - obj.relateddocument_set.create(relationship_id='conflrev',target=extracted) + obj.relateddocument_set.create(relationship_id="conflrev", target=extracted) else: - obj.relateddocument_set.create(relationship_id='conflrev',target=DocumentFactory(name=obj.name.replace('conflict-review-','draft-'),type_id='draft',group=Group.objects.get(type_id='individ'))) - + obj.relateddocument_set.create( + relationship_id="conflrev", + target=DocumentFactory( + name=obj.name.replace("conflict-review-", "draft-"), + type_id="draft", + group=Group.objects.get(type_id="individ"), + ), + ) @factory.post_generation def states(obj, create, extracted, **kwargs): @@ -271,53 +309,63 @@ def states(obj, create, extracted, **kwargs): for state in extracted: obj.set_state(state) else: - obj.set_state(State.objects.get(type_id='conflrev',slug='iesgeval')) + obj.set_state(State.objects.get(type_id="conflrev", slug="iesgeval")) + # This is very skeletal. It is enough for the tests that use it now, but when it's needed, it will need to be improved with, at least, a group generator that backs the object with a review team. class ReviewFactory(BaseDocumentFactory): - type_id = 'review' - name = factory.LazyAttribute(lambda o: 'review-doesnotexist-00-%s-%s'%(o.group.acronym,date_today().isoformat())) - group = factory.SubFactory('ietf.group.factories.GroupFactory',type_id='review') + type_id = "review" + name = factory.LazyAttribute( + lambda o: "review-doesnotexist-00-%s-%s" + % (o.group.acronym, date_today().isoformat()) + ) + group = factory.SubFactory("ietf.group.factories.GroupFactory", type_id="review") + class DocEventFactory(factory.django.DjangoModelFactory): class Meta: model = DocEvent - type = 'added_comment' - by = factory.SubFactory('ietf.person.factories.PersonFactory') - doc: Any = factory.SubFactory(DocumentFactory) # `Any` to appease mypy when a subclass overrides doc - desc = factory.Faker('sentence',nb_words=6) + type = "added_comment" + by = factory.SubFactory("ietf.person.factories.PersonFactory") + doc: Any = factory.SubFactory( + DocumentFactory + ) # `Any` to appease mypy when a subclass overrides doc + desc = factory.Faker("sentence", nb_words=6) @factory.lazy_attribute def rev(self): return self.doc.rev + class TelechatDocEventFactory(DocEventFactory): class Meta: model = TelechatDocEvent # note: this is evaluated at import time and not updated - all events will have the same telechat_date - telechat_date = timezone.now()+datetime.timedelta(days=14) - type = 'scheduled_for_telechat' + telechat_date = timezone.now() + datetime.timedelta(days=14) + type = "scheduled_for_telechat" + class NewRevisionDocEventFactory(DocEventFactory): class Meta: model = NewRevisionDocEvent - type = 'new_revision' - rev = '00' + type = "new_revision" + rev = "00" @factory.lazy_attribute def desc(self): - return 'New version available %s-%s'%(self.doc.name,self.rev) + return "New version available %s-%s" % (self.doc.name, self.rev) + class StateDocEventFactory(DocEventFactory): class Meta: model = StateDocEvent skip_postgeneration_save = True - type = 'changed_state' - state_type_id = 'draft-iesg' + type = "changed_state" + state_type_id = "draft-iesg" @factory.post_generation def state(obj, create, extracted, **kwargs): @@ -325,19 +373,20 @@ def state(obj, create, extracted, **kwargs): return if extracted: (state_type_id, state_slug) = extracted - obj.state = State.objects.get(type_id=state_type_id,slug=state_slug) + obj.state = State.objects.get(type_id=state_type_id, slug=state_slug) else: - obj.state = State.objects.get(type_id='draft-iesg',slug='ad-eval') + obj.state = State.objects.get(type_id="draft-iesg", slug="ad-eval") obj.save() + # All of these Ballot* factories are extremely skeletal. Flesh them out as needed by tests. class BallotTypeFactory(factory.django.DjangoModelFactory): class Meta: model = BallotType - django_get_or_create = ('slug','doc_type_id') + django_get_or_create = ("slug", "doc_type_id") - doc_type_id = 'draft' - slug = 'approve' + doc_type_id = "draft" + slug = "approve" class BallotDocEventFactory(DocEventFactory): @@ -345,54 +394,59 @@ class Meta: model = BallotDocEvent ballot_type = factory.SubFactory(BallotTypeFactory) - type = 'created_ballot' + type = "created_ballot" + class IRSGBallotDocEventFactory(BallotDocEventFactory): class Meta: model = IRSGBallotDocEvent duedate = timezone.now() + datetime.timedelta(days=14) - ballot_type = factory.SubFactory(BallotTypeFactory, slug='irsg-approve') + ballot_type = factory.SubFactory(BallotTypeFactory, slug="irsg-approve") + class BallotPositionDocEventFactory(DocEventFactory): class Meta: model = BallotPositionDocEvent - type = 'changed_ballot_position' + type = "changed_ballot_position" ballot = factory.SubFactory(BallotDocEventFactory) - doc = factory.SelfAttribute('ballot.doc') # point to same doc as the ballot - balloter = factory.SubFactory('ietf.person.factories.PersonFactory') - pos_id = 'discuss' + doc = factory.SelfAttribute("ballot.doc") # point to same doc as the ballot + balloter = factory.SubFactory("ietf.person.factories.PersonFactory") + pos_id = "discuss" + class DocumentActionHolderFactory(factory.django.DjangoModelFactory): class Meta: model = DocumentActionHolder - + document = factory.SubFactory(WgDraftFactory) - person = factory.SubFactory('ietf.person.factories.PersonFactory') + person = factory.SubFactory("ietf.person.factories.PersonFactory") + class DocumentAuthorFactory(factory.django.DjangoModelFactory): class Meta: model = DocumentAuthor document = factory.SubFactory(DocumentFactory) - person = factory.SubFactory('ietf.person.factories.PersonFactory') + person = factory.SubFactory("ietf.person.factories.PersonFactory") email = factory.LazyAttribute(lambda obj: obj.person.email()) - affiliation = factory.Faker('company') - country = factory.Faker('country') + affiliation = factory.Faker("company") + country = factory.Faker("country") order = factory.LazyAttribute(lambda o: o.document.documentauthor_set.count() + 1) + class WgDocumentAuthorFactory(DocumentAuthorFactory): document = factory.SubFactory(WgDraftFactory) + class BofreqEditorDocEventFactory(DocEventFactory): class Meta: model = BofreqEditorDocEvent skip_postgeneration_save = True type = "changed_editors" - doc = factory.SubFactory('ietf.doc.factories.BofreqFactory') - + doc = factory.SubFactory("ietf.doc.factories.BofreqFactory") @factory.post_generation def editors(obj, create, extracted, **kwargs): @@ -405,14 +459,14 @@ def editors(obj, create, extracted, **kwargs): obj.desc = f'Changed editors to {", ".join(obj.editors.values_list("name",flat=True)) or "(None)"}' obj.save() + class BofreqResponsibleDocEventFactory(DocEventFactory): class Meta: model = BofreqResponsibleDocEvent skip_postgeneration_save = True type = "changed_responsible" - doc = factory.SubFactory('ietf.doc.factories.BofreqFactory') - + doc = factory.SubFactory("ietf.doc.factories.BofreqFactory") @factory.post_generation def responsible(obj, create, extracted, **kwargs): @@ -421,36 +475,43 @@ def responsible(obj, create, extracted, **kwargs): if extracted: obj.responsible.set(extracted) else: - ad = RoleFactory(group__type_id='area',name_id='ad').person + ad = RoleFactory(group__type_id="area", name_id="ad").person obj.responsible.set([ad]) obj.desc = f'Changed responsible leadership to {", ".join(obj.responsible.values_list("name",flat=True)) or "(None)"}' - obj.save() + obj.save() + class BofreqFactory(BaseDocumentFactory): - type_id = 'bofreq' - title = factory.Faker('sentence') - name = factory.LazyAttribute(lambda o: 'bofreq-%s-%s'%(xslugify(o.requester_lastname), xslugify(o.title))) + type_id = "bofreq" + title = factory.Faker("sentence") + name = factory.LazyAttribute( + lambda o: "bofreq-%s-%s" % (xslugify(o.requester_lastname), xslugify(o.title)) + ) - bofreqeditordocevent = factory.RelatedFactory('ietf.doc.factories.BofreqEditorDocEventFactory','doc') - bofreqresponsibledocevent = factory.RelatedFactory('ietf.doc.factories.BofreqResponsibleDocEventFactory','doc') + bofreqeditordocevent = factory.RelatedFactory( + "ietf.doc.factories.BofreqEditorDocEventFactory", "doc" + ) + bofreqresponsibledocevent = factory.RelatedFactory( + "ietf.doc.factories.BofreqResponsibleDocEventFactory", "doc" + ) class Params: - requester_lastname = factory.Faker('last_name') + requester_lastname = factory.Faker("last_name") @factory.post_generation def states(obj, create, extracted, **kwargs): if not create: return if extracted: - for (state_type_id,state_slug) in extracted: - obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug)) + for state_type_id, state_slug in extracted: + obj.set_state(State.objects.get(type_id=state_type_id, slug=state_slug)) else: - obj.set_state(State.objects.get(type_id='bofreq',slug='proposed')) + obj.set_state(State.objects.get(type_id="bofreq", slug="proposed")) class ProceedingsMaterialDocFactory(BaseDocumentFactory): - type_id = 'procmaterials' - abstract = '' + type_id = "procmaterials" + abstract = "" expires = None @factory.post_generation @@ -458,42 +519,51 @@ def states(obj, create, extracted, **kwargs): if not create: return if extracted: - for (state_type_id,state_slug) in extracted: - obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug)) + for state_type_id, state_slug in extracted: + obj.set_state(State.objects.get(type_id=state_type_id, slug=state_slug)) else: - obj.set_state(State.objects.get(type_id='procmaterials', slug='active')) + obj.set_state(State.objects.get(type_id="procmaterials", slug="active")) + class DocExtResourceFactory(factory.django.DjangoModelFactory): - name = factory.Iterator(ExtResourceName.objects.filter(type_id='url')) - value = factory.Faker('url') - doc = factory.SubFactory('ietf.doc.factories.BaseDocumentFactory') + name = factory.Iterator(ExtResourceName.objects.filter(type_id="url")) + value = factory.Faker("url") + doc = factory.SubFactory("ietf.doc.factories.BaseDocumentFactory") + class Meta: model = DocExtResource + class EditorialDraftFactory(BaseDocumentFactory): - type_id = 'draft' - group = factory.SubFactory('ietf.group.factories.GroupFactory',acronym='rswg', type_id='edwg') - stream_id = 'editorial' + type_id = "draft" + group = factory.SubFactory( + "ietf.group.factories.GroupFactory", acronym="rswg", type_id="edwg" + ) + stream_id = "editorial" @factory.post_generation def states(obj, create, extracted, **kwargs): if not create: return if extracted: - for (state_type_id,state_slug) in extracted: - obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug)) - if not obj.get_state('draft-iesg'): - obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists')) + for state_type_id, state_slug in extracted: + obj.set_state(State.objects.get(type_id=state_type_id, slug=state_slug)) + if not obj.get_state("draft-iesg"): + obj.set_state(State.objects.get(type_id="draft-iesg", slug="idexists")) else: - obj.set_state(State.objects.get(type_id='draft',slug='active')) - obj.set_state(State.objects.get(type_id='draft-stream-editorial',slug='active')) - obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists')) + obj.set_state(State.objects.get(type_id="draft", slug="active")) + obj.set_state( + State.objects.get(type_id="draft-stream-editorial", slug="active") + ) + obj.set_state(State.objects.get(type_id="draft-iesg", slug="idexists")) + class EditorialRfcFactory(RgRfcFactory): pass - + + class StatementFactory(BaseDocumentFactory): type_id = "statement" title = factory.Faker("sentence") @@ -521,6 +591,7 @@ def states(obj, create, extracted, **kwargs): else: obj.set_state(State.objects.get(type_id="statement", slug="active")) + class SubseriesFactory(factory.django.DjangoModelFactory): class Meta: model = Document @@ -529,22 +600,27 @@ class Meta: @factory.lazy_attribute_sequence def name(self, n): return f"{self.type_id}{n}" - + @factory.post_generation def contains(obj, create, extracted, **kwargs): if not create: return if extracted: for doc in extracted: - obj.relateddocument_set.create(relationship_id="contains",target=doc) + obj.relateddocument_set.create(relationship_id="contains", target=doc) else: - obj.relateddocument_set.create(relationship_id="contains", target=RfcFactory()) + obj.relateddocument_set.create( + relationship_id="contains", target=RfcFactory() + ) + class BcpFactory(SubseriesFactory): - type_id="bcp" + type_id = "bcp" + class StdFactory(SubseriesFactory): - type_id="std" + type_id = "std" + class FyiFactory(SubseriesFactory): - type_id="fyi" + type_id = "fyi" diff --git a/ietf/doc/feeds.py b/ietf/doc/feeds.py index 500ed3cb18..b7e64a1b1f 100644 --- a/ietf/doc/feeds.py +++ b/ietf/doc/feeds.py @@ -264,9 +264,7 @@ def item_extra_kwargs(self, item): extra.update({"media_contents": media_contents}) extra.update({"doi": "10.17487/%s" % item.name.upper()}) - extra.update( - {"doiuri": "http://dx.doi.org/10.17487/%s" % item.name.upper()} - ) + extra.update({"doiuri": "http://dx.doi.org/10.17487/%s" % item.name.upper()}) # R104 Publisher (Mandatory - but we need a string from them first) extra.update({"dcterms_publisher": "rfc-editor.org"}) diff --git a/ietf/doc/fields.py b/ietf/doc/fields.py index 4a6922bf34..0751d089af 100644 --- a/ietf/doc/fields.py +++ b/ietf/doc/fields.py @@ -4,25 +4,37 @@ import json -from typing import Type # pyflakes:ignore +from typing import Type # pyflakes:ignore from django.utils.html import escape -from django.db import models # pyflakes:ignore +from django.db import models # pyflakes:ignore from django.db.models import Q from django.urls import reverse as urlreverse -import debug # pyflakes:ignore +import debug # pyflakes:ignore from ietf.doc.models import Document from ietf.doc.utils import uppercase_std_abbreviated_name from ietf.utils.fields import SearchableField + def select2_id_doc_name(model, objs): - return [{ - "id": o.pk, - "url": o.get_absolute_url() if model == Document else o.document.get_absolute_url(), - "text": escape(uppercase_std_abbreviated_name(o.name)), - } for o in objs] if objs else [] + return ( + [ + { + "id": o.pk, + "url": ( + o.get_absolute_url() + if model == Document + else o.document.get_absolute_url() + ), + "text": escape(uppercase_std_abbreviated_name(o.name)), + } + for o in objs + ] + if objs + else [] + ) def select2_id_doc_name_json(model, objs): @@ -30,8 +42,9 @@ def select2_id_doc_name_json(model, objs): class SearchableDocumentsField(SearchableField): - """Server-based multi-select field for choosing documents using select2.js. """ - model = Document # type: Type[models.Model] + """Server-based multi-select field for choosing documents using select2.js.""" + + model = Document # type: Type[models.Model] default_hint_text = "Type name to search for document" def __init__(self, doc_type="draft", *args, **kwargs): @@ -47,11 +60,9 @@ def get_model_instances(self, item_ids): Accepts both names and pks as IDs """ - names = [ i for i in item_ids if not i.isdigit() ] - ids = [ i for i in item_ids if i.isdigit() ] - objs = self.model.objects.filter( - Q(name__in=names)|Q(id__in=ids) - ) + names = [i for i in item_ids if not i.isdigit()] + ids = [i for i in item_ids if i.isdigit()] + objs = self.model.objects.filter(Q(name__in=names) | Q(id__in=ids)) return self.doc_type_filter(objs) def make_select2_data(self, model_instances): @@ -60,12 +71,16 @@ def make_select2_data(self, model_instances): def ajax_url(self): """Get the URL for AJAX searches""" - return urlreverse('ietf.doc.views_search.ajax_select2_search_docs', kwargs={ - "doc_type": self.doc_type, - "model_name": self.model.__name__.lower() - }) + return urlreverse( + "ietf.doc.views_search.ajax_select2_search_docs", + kwargs={ + "doc_type": self.doc_type, + "model_name": self.model.__name__.lower(), + }, + ) class SearchableDocumentField(SearchableDocumentsField): """Specialized to only return one Document""" + max_entries = 1 diff --git a/ietf/doc/forms.py b/ietf/doc/forms.py index 8a1e9ecb98..70d50a1af9 100644 --- a/ietf/doc/forms.py +++ b/ietf/doc/forms.py @@ -3,7 +3,7 @@ import datetime -import debug #pyflakes:ignore +import debug # pyflakes:ignore from django import forms from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.validators import validate_email @@ -19,26 +19,39 @@ from ietf.utils.timezone import date_today from ietf.utils.validators import validate_external_resource_value + class TelechatForm(forms.Form): - telechat_date = forms.TypedChoiceField(coerce=lambda x: datetime.datetime.strptime(x, '%Y-%m-%d').date(), empty_value=None, required=False, help_text="Page counts are the current page counts for the telechat, before this telechat date edit is made.") + telechat_date = forms.TypedChoiceField( + coerce=lambda x: datetime.datetime.strptime(x, "%Y-%m-%d").date(), + empty_value=None, + required=False, + help_text="Page counts are the current page counts for the telechat, before this telechat date edit is made.", + ) returning_item = forms.BooleanField(required=False) def __init__(self, *args, **kwargs): super(self.__class__, self).__init__(*args, **kwargs) - dates = [d.date for d in TelechatDate.objects.active().order_by('date')] - init = kwargs['initial'].get("telechat_date") + dates = [d.date for d in TelechatDate.objects.active().order_by("date")] + init = kwargs["initial"].get("telechat_date") if init and init not in dates: dates.insert(0, init) self.page_count = {} choice_display = {} for d in dates: - self.page_count[d] = telechat_page_count(date=d).for_approval - choice_display[d] = '%s (%s pages)' % (d.strftime("%Y-%m-%d"),self.page_count[d]) - if d - date_today() < datetime.timedelta(days=13): - choice_display[d] += ' : WARNING - this may not leave enough time for directorate reviews!' - self.fields['telechat_date'].choices = [("", "(not on agenda)")] + [(d, choice_display[d]) for d in dates] + self.page_count[d] = telechat_page_count(date=d).for_approval + choice_display[d] = "%s (%s pages)" % ( + d.strftime("%Y-%m-%d"), + self.page_count[d], + ) + if d - date_today() < datetime.timedelta(days=13): + choice_display[ + d + ] += " : WARNING - this may not leave enough time for directorate reviews!" + self.fields["telechat_date"].choices = [("", "(not on agenda)")] + [ + (d, choice_display[d]) for d in dates + ] class DocAuthorForm(forms.Form): @@ -46,34 +59,48 @@ class DocAuthorForm(forms.Form): email = forms.ModelChoiceField(queryset=Email.objects.none(), required=False) affiliation = forms.CharField(max_length=100, required=False) country = forms.CharField(max_length=255, required=False) - + def __init__(self, *args, **kwargs): super(DocAuthorForm, self).__init__(*args, **kwargs) person = self.data.get( - self.add_prefix('person'), - self.get_initial_for_field(self.fields['person'], 'person') + self.add_prefix("person"), + self.get_initial_for_field(self.fields["person"], "person"), ) if person: - self.fields['email'].queryset = Email.objects.filter(person=person) + self.fields["email"].queryset = Email.objects.filter(person=person) + class DocAuthorChangeBasisForm(forms.Form): - basis = forms.CharField(max_length=255, - label='Reason for change', - help_text='What is the source or reasoning for the changes to the author list?') - + basis = forms.CharField( + max_length=255, + label="Reason for change", + help_text="What is the source or reasoning for the changes to the author list?", + ) + + class AdForm(forms.Form): - ad = forms.ModelChoiceField(Person.objects.filter(role__name="ad", role__group__state="active", role__group__type='area').order_by('name'), - label="Shepherding AD", empty_label="(None)", required=True) + ad = forms.ModelChoiceField( + Person.objects.filter( + role__name="ad", role__group__state="active", role__group__type="area" + ).order_by("name"), + label="Shepherding AD", + empty_label="(None)", + required=True, + ) def __init__(self, *args, **kwargs): super(self.__class__, self).__init__(*args, **kwargs) # if previous AD is now ex-AD, append that person to the list - ad_pk = self.initial.get('ad') - choices = self.fields['ad'].choices + ad_pk = self.initial.get("ad") + choices = self.fields["ad"].choices if ad_pk and ad_pk not in [pk for pk, name in choices]: - self.fields['ad'].choices = list(choices) + [("", "-------"), (ad_pk, Person.objects.get(pk=ad_pk).plain_name())] + self.fields["ad"].choices = list(choices) + [ + ("", "-------"), + (ad_pk, Person.objects.get(pk=ad_pk).plain_name()), + ] + class NotifyForm(forms.Form): notify = forms.CharField( @@ -90,7 +117,7 @@ def clean_notify(self): # python set doesn't preserve order, so in an attempt to mostly preserve the order of what was entered, we'll use # a dict (whose keys are guaranteed to be ordered) to cull out duplicates - nameaddrs=dict() + nameaddrs = dict() duplicate_addrspecs = set() bad_nameaddrs = [] for nameaddr in self.cleaned_data["notify"].replace("\n", ",").split(","): @@ -101,7 +128,7 @@ def clean_notify(self): if stripped[-1] != ">": bad_nameaddrs.append(nameaddr) continue - addrspec = stripped[stripped.find("<")+1:-1] + addrspec = stripped[stripped.find("<") + 1 : -1] else: addrspec = stripped try: @@ -115,100 +142,127 @@ def clean_notify(self): nameaddrs[addrspec] = stripped error_messages = [] if len(duplicate_addrspecs) != 0: - error_messages.append(f'Duplicate addresses: {", ".join(duplicate_addrspecs)}') + error_messages.append( + f'Duplicate addresses: {", ".join(duplicate_addrspecs)}' + ) if len(bad_nameaddrs) != 0: error_messages.append(f'Invalid addresses: {", ".join(bad_nameaddrs)}') if len(error_messages) != 0: raise ValidationError(" and ".join(error_messages)) return ", ".join(nameaddrs.values()) + class ActionHoldersForm(forms.Form): action_holders = SearchablePersonsField(required=False) reason = forms.CharField( - label='Reason for change', + label="Reason for change", required=False, max_length=255, strip=True, ) + IESG_APPROVED_STATE_LIST = ("ann", "rfcqueue", "pub") + class AddDownrefForm(forms.Form): rfc = SearchableDocumentField( - label="Referenced RFC", - help_text="The RFC that is approved for downref", - required=True, - doc_type="rfc") + label="Referenced RFC", + help_text="The RFC that is approved for downref", + required=True, + doc_type="rfc", + ) drafts = SearchableDocumentsField( - label="Internet-Drafts that makes the reference", - help_text="The Internet-Drafts that approve the downref in their Last Call", - required=True) + label="Internet-Drafts that makes the reference", + help_text="The Internet-Drafts that approve the downref in their Last Call", + required=True, + ) def clean_rfc(self): - if 'rfc' not in self.cleaned_data: - raise forms.ValidationError("Please provide a referenced RFC and a referencing Internet-Draft") + if "rfc" not in self.cleaned_data: + raise forms.ValidationError( + "Please provide a referenced RFC and a referencing Internet-Draft" + ) - rfc = self.cleaned_data['rfc'] + rfc = self.cleaned_data["rfc"] if rfc.type_id != "rfc": raise forms.ValidationError("Cannot find the RFC: " + rfc.name) return rfc def clean_drafts(self): - if 'drafts' not in self.cleaned_data: - raise forms.ValidationError("Please provide a referenced RFC and a referencing Internet-Draft") + if "drafts" not in self.cleaned_data: + raise forms.ValidationError( + "Please provide a referenced RFC and a referencing Internet-Draft" + ) v_err_names = [] - drafts = self.cleaned_data['drafts'] + drafts = self.cleaned_data["drafts"] for d in drafts: state = d.get_state("draft-iesg") if not state or state.slug not in IESG_APPROVED_STATE_LIST: v_err_names.append(d.name) if v_err_names: - raise forms.ValidationError("Internet-Draft is not yet approved: " + ", ".join(v_err_names)) + raise forms.ValidationError( + "Internet-Draft is not yet approved: " + ", ".join(v_err_names) + ) return drafts def clean(self): - if 'rfc' not in self.cleaned_data or 'drafts' not in self.cleaned_data: - raise forms.ValidationError("Please provide a referenced RFC and a referencing Internet-Draft") + if "rfc" not in self.cleaned_data or "drafts" not in self.cleaned_data: + raise forms.ValidationError( + "Please provide a referenced RFC and a referencing Internet-Draft" + ) v_err_pairs = [] - rfc = self.cleaned_data['rfc'] - drafts = self.cleaned_data['drafts'] + rfc = self.cleaned_data["rfc"] + drafts = self.cleaned_data["drafts"] for d in drafts: - if RelatedDocument.objects.filter(source=d, target=rfc, relationship_id='downref-approval'): + if RelatedDocument.objects.filter( + source=d, target=rfc, relationship_id="downref-approval" + ): v_err_pairs.append(f"{d.name} --> RFC {rfc.rfc_number}") if v_err_pairs: - raise forms.ValidationError("Downref is already in the registry: " + ", ".join(v_err_pairs)) + raise forms.ValidationError( + "Downref is already in the registry: " + ", ".join(v_err_pairs) + ) - if 'save_downref_anyway' not in self.data: - # this check is skipped if the save_downref_anyway button is used + if "save_downref_anyway" not in self.data: + # this check is skipped if the save_downref_anyway button is used v_err_refnorm = "" for d in drafts: - if not RelatedDocument.objects.filter(source=d, target=rfc, relationship_id='refnorm'): + if not RelatedDocument.objects.filter( + source=d, target=rfc, relationship_id="refnorm" + ): if v_err_refnorm: v_err_refnorm = v_err_refnorm + " or " + d.name else: v_err_refnorm = d.name if v_err_refnorm: v_err_refnorm_prefix = f"There does not seem to be a normative reference to RFC {rfc.rfc_number} by " - raise forms.ValidationError(v_err_refnorm_prefix + v_err_refnorm) + raise forms.ValidationError(v_err_refnorm_prefix + v_err_refnorm) class ExtResourceForm(forms.Form): - resources = forms.CharField(widget=forms.Textarea, label="Additional Resources", required=False, - help_text=("Format: 'tag value (Optional description)'." - " Separate multiple entries with newline. When the value is a URL, use https:// where possible.") ) + resources = forms.CharField( + widget=forms.Textarea, + label="Additional Resources", + required=False, + help_text=( + "Format: 'tag value (Optional description)'." + " Separate multiple entries with newline. When the value is a URL, use https:// where possible." + ), + ) def __init__(self, *args, initial=None, extresource_model=None, **kwargs): self.extresource_model = extresource_model if initial: kwargs = kwargs.copy() - resources = initial.get('resources') + resources = initial.get("resources") if resources is not None and not isinstance(resources, str): initial = initial.copy() # Convert objects to string representation - initial['resources'] = self.format_resources(resources) - kwargs['initial'] = initial + initial["resources"] = self.format_resources(resources) + kwargs["initial"] = initial super(ExtResourceForm, self).__init__(*args, **kwargs) @staticmethod @@ -226,18 +280,28 @@ def clean_resources(self): validated using validate_external_resource_value(). Further interpretation of the resource is performed int he clean() method. """ - lines = [x.strip() for x in self.cleaned_data["resources"].splitlines() if x.strip()] + lines = [ + x.strip() for x in self.cleaned_data["resources"].splitlines() if x.strip() + ] errors = [] for l in lines: parts = l.split() if len(parts) == 1: - errors.append("Too few fields: Expected at least tag and value: '%s'" % l) + errors.append( + "Too few fields: Expected at least tag and value: '%s'" % l + ) elif len(parts) >= 2: name_slug = parts[0] try: name = ExtResourceName.objects.get(slug=name_slug) except ObjectDoesNotExist: - errors.append("Bad tag in '%s': Expected one of %s" % (l, ', '.join([ o.slug for o in ExtResourceName.objects.all() ]))) + errors.append( + "Bad tag in '%s': Expected one of %s" + % ( + l, + ", ".join([o.slug for o in ExtResourceName.objects.all()]), + ) + ) continue value = parts[1] try: @@ -257,15 +321,18 @@ def clean(self): cleaned_data = super(ExtResourceForm, self).clean() cleaned_resources = [] cls = self.extresource_model or DocExtResource - for crs in cleaned_data.get('resources', []): - cleaned_resources.append( - cls.from_form_entry_str(crs) - ) - cleaned_data['resources'] = cleaned_resources + for crs in cleaned_data.get("resources", []): + cleaned_resources.append(cls.from_form_entry_str(crs)) + cleaned_data["resources"] = cleaned_resources @staticmethod def valid_resource_tags(): - return ExtResourceName.objects.all().order_by('slug').values_list('slug', flat=True) + return ( + ExtResourceName.objects.all() + .order_by("slug") + .values_list("slug", flat=True) + ) + class InvestigateForm(forms.Form): name_fragment = forms.CharField( @@ -286,5 +353,7 @@ def clean_name_fragment(self): # Requiring this will help protect against the secretariat unintentionally # matching every draft. if any(c in name_fragment for c in disallowed_characters): - raise ValidationError(f"The following characters are disallowed: {', '.join(disallowed_characters)}") + raise ValidationError( + f"The following characters are disallowed: {', '.join(disallowed_characters)}" + ) return name_fragment diff --git a/ietf/doc/lastcall.py b/ietf/doc/lastcall.py index dd38fd3909..197cbb9b63 100644 --- a/ietf/doc/lastcall.py +++ b/ietf/doc/lastcall.py @@ -6,8 +6,16 @@ from ietf.doc.models import IESG_SUBSTATE_TAGS from ietf.person.models import Person from ietf.doc.utils import add_state_change_event, update_action_holders -from ietf.doc.mails import generate_ballot_writeup, generate_approval_mail, generate_last_call_announcement -from ietf.doc.mails import send_last_call_request, email_last_call_expired, email_last_call_expired_with_downref +from ietf.doc.mails import ( + generate_ballot_writeup, + generate_approval_mail, + generate_last_call_announcement, +) +from ietf.doc.mails import ( + send_last_call_request, + email_last_call_expired, + email_last_call_expired_with_downref, +) from ietf.utils.timezone import date_today, DEADLINE_TZINFO @@ -23,7 +31,7 @@ def request_last_call(request, doc): e.save() send_last_call_request(request, doc) - + e = DocEvent() e.type = "requested_last_call" e.by = request.user.person @@ -32,25 +40,36 @@ def request_last_call(request, doc): e.desc = "Last call was requested" e.save() + def get_expired_last_calls(): - for d in Document.objects.filter(Q(states__type="draft-iesg", states__slug="lc") - | Q(states__type="statchg", states__slug="in-lc")): + for d in Document.objects.filter( + Q(states__type="draft-iesg", states__slug="lc") + | Q(states__type="statchg", states__slug="in-lc") + ): e = d.latest_event(LastCallDocEvent, type="sent_last_call") - if e and e.expires.astimezone(DEADLINE_TZINFO).date() <= date_today(DEADLINE_TZINFO): + if e and e.expires.astimezone(DEADLINE_TZINFO).date() <= date_today( + DEADLINE_TZINFO + ): yield d + def expire_last_call(doc): - if doc.type_id == 'draft': + if doc.type_id == "draft": new_state = State.objects.get(used=True, type="draft-iesg", slug="writeupw") e = doc.latest_event(WriteupDocEvent, type="changed_ballot_writeup_text") - if e and "Relevant content can frequently be found in the abstract" not in e.text: + if ( + e + and "Relevant content can frequently be found in the abstract" not in e.text + ): # if boiler-plate text has been removed, we assume the # write-up has been written new_state = State.objects.get(used=True, type="draft-iesg", slug="goaheadw") - elif doc.type_id == 'statchg': + elif doc.type_id == "statchg": new_state = State.objects.get(used=True, type="statchg", slug="goahead") else: - raise ValueError("Unexpected document type to expire_last_call(): %s" % doc.type) + raise ValueError( + "Unexpected document type to expire_last_call(): %s" % doc.type + ) prev_state = doc.get_state(new_state.type_id) doc.set_state(new_state) @@ -60,17 +79,21 @@ def expire_last_call(doc): system = Person.objects.get(name="(System)") events = [] - e = add_state_change_event(doc, system, prev_state, new_state, prev_tags=prev_tags, new_tags=[]) + e = add_state_change_event( + doc, system, prev_state, new_state, prev_tags=prev_tags, new_tags=[] + ) if e: events.append(e) - e = update_action_holders(doc, prev_state, new_state, prev_tags=prev_tags, new_tags=[]) + e = update_action_holders( + doc, prev_state, new_state, prev_tags=prev_tags, new_tags=[] + ) if e: events.append(e) doc.save_with_history(events) email_last_call_expired(doc) - if doc.type_id == 'draft': + if doc.type_id == "draft": lc_text = doc.latest_event(LastCallDocEvent, type="sent_last_call").desc if "document makes the following downward references" in lc_text: - email_last_call_expired_with_downref(doc, lc_text) + email_last_call_expired_with_downref(doc, lc_text) diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index ddecbb6b54..6915e45c32 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -1,6 +1,6 @@ # Copyright The IETF Trust 2010-2020, All Rights Reserved # -*- coding: utf-8 -*- -# generation of mails +# generation of mails import datetime @@ -13,7 +13,7 @@ from django.utils import timezone from django.utils.encoding import force_str -import debug # pyflakes:ignore +import debug # pyflakes:ignore from ietf.doc.templatetags.mail_filters import std_level_prompt from ietf.utils import log @@ -29,57 +29,67 @@ def email_state_changed(request, doc, text, mailtrigger_id=None): - (to,cc) = gather_address_lists(mailtrigger_id or 'doc_state_edited',doc=doc) + (to, cc) = gather_address_lists(mailtrigger_id or "doc_state_edited", doc=doc) if not to: return - + text = strip_tags(text) - send_mail(request, to, None, - "Datatracker State Update Notice: %s" % doc.file_tag(), - "doc/mail/state_changed_email.txt", - dict(text=text, - url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), - cc=cc) - + send_mail( + request, + to, + None, + "Datatracker State Update Notice: %s" % doc.file_tag(), + "doc/mail/state_changed_email.txt", + dict(text=text, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), + cc=cc, + ) + + def email_ad_approved_doc(request, doc, text): - to = "iesg@iesg.org" - bcc = "iesg-secretary@ietf.org" - frm = request.user.person.formatted_email() - send_mail(request, to, frm, - "Approved: %s" % doc.filename_with_rev(), - "doc/mail/ad_approval_email.txt", - dict(text=text, - docname=doc.filename_with_rev()), - bcc=bcc) + to = "iesg@iesg.org" + bcc = "iesg-secretary@ietf.org" + frm = request.user.person.formatted_email() + send_mail( + request, + to, + frm, + "Approved: %s" % doc.filename_with_rev(), + "doc/mail/ad_approval_email.txt", + dict(text=text, docname=doc.filename_with_rev()), + bcc=bcc, + ) + def email_ad_approved_conflict_review(request, review, ok_to_publish): """Email notification when AD approves a conflict review""" - conflictdoc = review.relateddocument_set.get(relationship__slug='conflrev').target + conflictdoc = review.relateddocument_set.get(relationship__slug="conflrev").target (to, cc) = gather_address_lists("ad_approved_conflict_review") frm = request.user.person.formatted_email() - send_mail(request, - to, - frm, - "Approved: %s" % review.title, - "doc/conflict_review/ad_approval_pending_email.txt", - dict(ok_to_publish=ok_to_publish, - review=review, - conflictdoc=conflictdoc), - cc=cc) + send_mail( + request, + to, + frm, + "Approved: %s" % review.title, + "doc/conflict_review/ad_approval_pending_email.txt", + dict(ok_to_publish=ok_to_publish, review=review, conflictdoc=conflictdoc), + cc=cc, + ) + def email_ad_approved_status_change(request, status_change, related_doc_info): """Email notification when AD approves a status change""" (to, cc) = gather_address_lists("ad_approved_status_change") frm = request.user.person.formatted_email() - send_mail(request, - to, - frm, - "Approved: %s" % status_change.title, - "doc/status_change/ad_approval_pending_email.txt", - dict( - related_doc_info=related_doc_info - ), - cc=cc) + send_mail( + request, + to, + frm, + "Approved: %s" % status_change.title, + "doc/status_change/ad_approval_pending_email.txt", + dict(related_doc_info=related_doc_info), + cc=cc, + ) + def email_stream_changed(request, doc, old_stream, new_stream, text=""): """Email the change text to the notify group and to the stream chairs""" @@ -88,81 +98,114 @@ def email_stream_changed(request, doc, old_stream, new_stream, text=""): streams.append(old_stream.slug) if new_stream: streams.append(new_stream.slug) - (to,cc) = gather_address_lists('doc_stream_changed',doc=doc,streams=streams) + (to, cc) = gather_address_lists("doc_stream_changed", doc=doc, streams=streams) if not to: return - + if not text: text = "Stream changed to %s from %s" % (new_stream, old_stream) text = strip_tags(text) - send_mail(request, to, None, - "I-D Tracker Stream Change Notice: %s" % doc.file_tag(), - "doc/mail/stream_changed_email.txt", - dict(text=text, - url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), - cc=cc) + send_mail( + request, + to, + None, + "I-D Tracker Stream Change Notice: %s" % doc.file_tag(), + "doc/mail/stream_changed_email.txt", + dict(text=text, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), + cc=cc, + ) + def email_pulled_from_rfc_queue(request, doc, comment, prev_state, next_state): - extra=extra_automation_headers(doc) - addrs = gather_address_lists('doc_pulled_from_rfc_queue',doc=doc) - extra['Cc'] = addrs.cc - send_mail(request, addrs.to , None, - "%s changed state from %s to %s" % (doc.name, prev_state.name, next_state.name), - "doc/mail/pulled_from_rfc_queue_email.txt", - dict(doc=doc, - prev_state=prev_state, - next_state=next_state, - comment=comment, - url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), - extra=extra) + extra = extra_automation_headers(doc) + addrs = gather_address_lists("doc_pulled_from_rfc_queue", doc=doc) + extra["Cc"] = addrs.cc + send_mail( + request, + addrs.to, + None, + "%s changed state from %s to %s" % (doc.name, prev_state.name, next_state.name), + "doc/mail/pulled_from_rfc_queue_email.txt", + dict( + doc=doc, + prev_state=prev_state, + next_state=next_state, + comment=comment, + url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), + ), + extra=extra, + ) + def email_iesg_processing_document(request, doc, changes): - addrs = gather_address_lists('doc_iesg_processing_started',doc=doc) + addrs = gather_address_lists("doc_iesg_processing_started", doc=doc) tagless_changes = [] for c in changes: tagless_changes.append(strip_tags(c)) - send_mail(request, addrs.to, None, - 'IESG processing details changed for %s' % doc.name, - 'doc/mail/email_iesg_processing.txt', - dict(doc=doc, - changes=tagless_changes, - url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), - cc=addrs.cc) + send_mail( + request, + addrs.to, + None, + "IESG processing details changed for %s" % doc.name, + "doc/mail/email_iesg_processing.txt", + dict( + doc=doc, + changes=tagless_changes, + url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), + ), + cc=addrs.cc, + ) + def email_remind_action_holders(request, doc, note=None): - addrs = gather_address_lists('doc_remind_action_holders', doc=doc) + addrs = gather_address_lists("doc_remind_action_holders", doc=doc) log.assertion( - 'not doc.action_holders.exclude(email__in=addrs.to).exists()', - note='All action holders should receive a reminder email. Failed for %s.' % doc.name, - ) - send_mail(request, addrs.to, None, - 'Reminder: action needed for %s' % doc.display_name(), - 'doc/mail/remind_action_holders_mail.txt', - dict( - doc=doc, - doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), - note=note, - ), - cc=addrs.cc) + "not doc.action_holders.exclude(email__in=addrs.to).exists()", + note="All action holders should receive a reminder email. Failed for %s." + % doc.name, + ) + send_mail( + request, + addrs.to, + None, + "Reminder: action needed for %s" % doc.display_name(), + "doc/mail/remind_action_holders_mail.txt", + dict( + doc=doc, + doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), + note=note, + ), + cc=addrs.cc, + ) + def html_to_text(html): - return strip_tags(html.replace("<", "<").replace(">", ">").replace("&", "&").replace("
", "\n")) - + return strip_tags( + html.replace("<", "<") + .replace(">", ">") + .replace("&", "&") + .replace("
", "\n") + ) + + def email_update_telechat(request, doc, text): - (to, cc) = gather_address_lists('doc_telechat_details_changed',doc=doc) + (to, cc) = gather_address_lists("doc_telechat_details_changed", doc=doc) if not to: return - + text = strip_tags(text) - send_mail(request, to, None, - "Telechat update notice: %s" % doc.file_tag(), - "doc/mail/update_telechat.txt", - dict(text=text, - url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), - cc=cc) + send_mail( + request, + to, + None, + "Telechat update notice: %s" % doc.file_tag(), + "doc/mail/update_telechat.txt", + dict(text=text, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), + cc=cc, + ) def generate_ballot_writeup(request, doc): @@ -175,11 +218,14 @@ def generate_ballot_writeup(request, doc): e.doc = doc e.rev = doc.rev e.desc = "Ballot writeup was generated" - e.text = force_str(render_to_string("doc/mail/ballot_writeup.txt", {'iana': iana, 'doc': doc })) + e.text = force_str( + render_to_string("doc/mail/ballot_writeup.txt", {"iana": iana, "doc": doc}) + ) # caller is responsible for saving, if necessary return e - + + def generate_ballot_rfceditornote(request, doc): e = WriteupDocEvent() e.type = "changed_ballot_rfceditornote_text" @@ -189,42 +235,60 @@ def generate_ballot_rfceditornote(request, doc): e.desc = "RFC Editor Note for ballot was generated" e.text = force_str(render_to_string("doc/mail/ballot_rfceditornote.txt")) e.save() - + return e + def generate_last_call_announcement(request, doc): expiration_date = date_today(DEADLINE_TZINFO) + datetime.timedelta(days=14) if doc.group.type_id in ("individ", "area"): group = "an individual submitter" expiration_date += datetime.timedelta(days=14) else: - group = "the %s %s (%s)" % (doc.group.name, doc.group.type.name, doc.group.acronym) + group = "the %s %s (%s)" % ( + doc.group.name, + doc.group.type.name, + doc.group.acronym, + ) doc.filled_title = textwrap.fill(doc.title, width=70, subsequent_indent=" " * 3) - + iprs = iprs_from_docs(related_docs(Document.objects.get(name=doc.name))) if iprs: - ipr_links = [ urlreverse("ietf.ipr.views.show", kwargs=dict(id=i.id)) for i in iprs] - ipr_links = [ settings.IDTRACKER_BASE_URL+url if not url.startswith("http") else url for url in ipr_links ] + ipr_links = [ + urlreverse("ietf.ipr.views.show", kwargs=dict(id=i.id)) for i in iprs + ] + ipr_links = [ + settings.IDTRACKER_BASE_URL + url if not url.startswith("http") else url + for url in ipr_links + ] else: ipr_links = None - downrefs = [rel for rel in doc.relateddocument_set.all() if rel.is_downref() and not rel.is_approved_downref()] - - addrs = gather_address_lists('last_call_issued',doc=doc).as_strings() - mail = render_to_string("doc/mail/last_call_announcement.txt", - dict(doc=doc, - doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url() + "ballot/", - expiration_date=expiration_date.strftime("%Y-%m-%d"), #.strftime("%B %-d, %Y"), - to=addrs.to, - cc=addrs.cc, - group=group, - docs=[ doc ], - urls=[ settings.IDTRACKER_BASE_URL + doc.get_absolute_url() ], - ipr_links=ipr_links, - downrefs=downrefs, - ) - ) + downrefs = [ + rel + for rel in doc.relateddocument_set.all() + if rel.is_downref() and not rel.is_approved_downref() + ] + + addrs = gather_address_lists("last_call_issued", doc=doc).as_strings() + mail = render_to_string( + "doc/mail/last_call_announcement.txt", + dict( + doc=doc, + doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url() + "ballot/", + expiration_date=expiration_date.strftime( + "%Y-%m-%d" + ), # .strftime("%B %-d, %Y"), + to=addrs.to, + cc=addrs.cc, + group=group, + docs=[doc], + urls=[settings.IDTRACKER_BASE_URL + doc.get_absolute_url()], + ipr_links=ipr_links, + downrefs=downrefs, + ), + ) e = WriteupDocEvent() e.type = "changed_last_call_text" @@ -236,12 +300,15 @@ def generate_last_call_announcement(request, doc): # caller is responsible for saving, if necessary return e - + DO_NOT_PUBLISH_IESG_STATES = ("nopubadw", "nopubanw") + def generate_approval_mail(request, doc): - if doc.get_state_slug("draft-iesg") in DO_NOT_PUBLISH_IESG_STATES or doc.stream_id in ('ise','irtf'): + if doc.get_state_slug( + "draft-iesg" + ) in DO_NOT_PUBLISH_IESG_STATES or doc.stream_id in ("ise", "irtf"): mail = generate_approval_mail_rfc_editor(request, doc) else: mail = generate_approval_mail_approved(request, doc) @@ -257,6 +324,7 @@ def generate_approval_mail(request, doc): # caller is responsible for saving, if necessary return e + def generate_approval_mail_approved(request, doc): if doc.intended_std_level_id in ("std", "ds", "ps", "bcp"): @@ -266,7 +334,13 @@ def generate_approval_mail_approved(request, doc): # the second check catches some area working groups (like # Transport Area Working Group) - if doc.group.type_id not in ("area", "individ", "ag", "rg", "rag") and not doc.group.name.endswith("Working Group"): + if doc.group.type_id not in ( + "area", + "individ", + "ag", + "rg", + "rag", + ) and not doc.group.name.endswith("Working Group"): doc.group.name_with_wg = doc.group.name + " Working Group" else: doc.group.name_with_wg = doc.group.name @@ -277,55 +351,77 @@ def generate_approval_mail_approved(request, doc): made_by = "This document has been reviewed in the IETF but is not the product of an IETF Working Group." else: made_by = "This document is the product of the %s." % doc.group.name_with_wg - - responsible_directors = set([doc.ad,]) - if doc.group.type_id not in ("individ","area"): - responsible_directors.update([x.person for x in Role.objects.filter(group=doc.group.parent,name='ad')]) + + responsible_directors = set( + [ + doc.ad, + ] + ) + if doc.group.type_id not in ("individ", "area"): + responsible_directors.update( + [x.person for x in Role.objects.filter(group=doc.group.parent, name="ad")] + ) responsible_directors = [x.plain_name() for x in responsible_directors if x] - - if len(responsible_directors)>1: - contacts = "The IESG contact persons are "+", ".join(responsible_directors[:-1])+" and "+responsible_directors[-1]+"." + + if len(responsible_directors) > 1: + contacts = ( + "The IESG contact persons are " + + ", ".join(responsible_directors[:-1]) + + " and " + + responsible_directors[-1] + + "." + ) else: contacts = "The IESG contact person is %s." % responsible_directors[0] doc_type = "RFC" if doc.get_state_slug() == "rfc" else "Internet-Draft" - - addrs = gather_address_lists('ballot_approved_ietf_stream',doc=doc).as_strings() - return render_to_string("doc/mail/approval_mail.txt", - dict(doc=doc, - docs=[doc], - doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), - to = addrs.to, - cc = addrs.cc, - doc_type=doc_type, - made_by=made_by, - contacts=contacts, - action_type=action_type, - ) - ) + + addrs = gather_address_lists("ballot_approved_ietf_stream", doc=doc).as_strings() + return render_to_string( + "doc/mail/approval_mail.txt", + dict( + doc=doc, + docs=[doc], + doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), + to=addrs.to, + cc=addrs.cc, + doc_type=doc_type, + made_by=made_by, + contacts=contacts, + action_type=action_type, + ), + ) + def generate_approval_mail_rfc_editor(request, doc): # This is essentially dead code - it is only exercised if the IESG ballots on some other stream's document, # which does not happen now that we have conflict reviews. disapproved = doc.get_state_slug("draft-iesg") in DO_NOT_PUBLISH_IESG_STATES doc_type = "RFC" if doc.get_state_slug() == "rfc" else "Internet-Draft" - addrs = gather_address_lists('ballot_approved_conflrev', doc=doc).as_strings() - - return render_to_string("doc/mail/approval_mail_rfc_editor.txt", - dict(doc=doc, - doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), - doc_type=doc_type, - disapproved=disapproved, - to = addrs.to, - cc = addrs.cc, - ) - ) + addrs = gather_address_lists("ballot_approved_conflrev", doc=doc).as_strings() + + return render_to_string( + "doc/mail/approval_mail_rfc_editor.txt", + dict( + doc=doc, + doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), + doc_type=doc_type, + disapproved=disapproved, + to=addrs.to, + cc=addrs.cc, + ), + ) + def generate_publication_request(request, doc): group_description = "" if doc.group and doc.group.acronym != "none": group_description = doc.group.name - if doc.group.type_id not in ("ietf", "irtf", "iab",): + 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") @@ -344,31 +440,40 @@ def generate_publication_request(request, doc): e = doc.latest_event(WriteupDocEvent, type="changed_rfc_editor_note_text") rfcednote = e.text if e else "" - return render_to_string("doc/mail/publication_request.txt", - dict(doc=doc, - doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), - group_description=group_description, - approving_body=approving_body, - consensus_body=consensus_body, - consensus=consensus, - rfc_editor_note=rfcednote, - ) - ) + return render_to_string( + "doc/mail/publication_request.txt", + dict( + doc=doc, + doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), + group_description=group_description, + approving_body=approving_body, + consensus_body=consensus_body, + consensus=consensus, + rfc_editor_note=rfcednote, + ), + ) + def send_last_call_request(request, doc): - (to, cc) = gather_address_lists('last_call_requested',doc=doc) + (to, cc) = gather_address_lists("last_call_requested", doc=doc) frm = '"DraftTracker Mail System" ' - - send_mail(request, to, frm, - "Last Call: %s" % doc.file_tag(), - "doc/mail/last_call_request.txt", - dict(docs=[doc], - doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), - ), - cc=cc) + + send_mail( + request, + to, + frm, + "Last Call: %s" % doc.file_tag(), + "doc/mail/last_call_request.txt", + dict( + docs=[doc], + doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), + ), + cc=cc, + ) + def email_resurrect_requested(request, doc, by): - (to, cc) = gather_address_lists('resurrection_requested',doc=doc) + (to, cc) = gather_address_lists("resurrection_requested", doc=doc) if by.role_set.filter(name="secr", group__acronym="secretariat"): e = by.role_email("secr", group="secretariat") @@ -376,80 +481,91 @@ def email_resurrect_requested(request, doc, by): e = by.role_email("ad") frm = e.address - send_mail(request, to, e.formatted_email(), - "I-D Resurrection Request", - "doc/mail/resurrect_request_email.txt", - dict(doc=doc, - by=frm, - url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), - cc=cc) + send_mail( + request, + to, + e.formatted_email(), + "I-D Resurrection Request", + "doc/mail/resurrect_request_email.txt", + dict(doc=doc, by=frm, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), + cc=cc, + ) + def email_resurrection_completed(request, doc, requester): - (to, cc) = gather_address_lists('resurrection_completed',doc=doc) + (to, cc) = gather_address_lists("resurrection_completed", doc=doc) frm = "I-D Administrator " - send_mail(request, to, frm, - "I-D Resurrection Completed - %s" % doc.file_tag(), - "doc/mail/resurrect_completed_email.txt", - dict(doc=doc, - by=frm, - url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), - cc=cc) + send_mail( + request, + to, + frm, + "I-D Resurrection Completed - %s" % doc.file_tag(), + "doc/mail/resurrect_completed_email.txt", + dict(doc=doc, by=frm, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), + cc=cc, + ) + def email_ballot_deferred(request, doc, by, telechat_date): - (to, cc) = gather_address_lists('ballot_deferred',doc=doc) + (to, cc) = gather_address_lists("ballot_deferred", doc=doc) frm = "DraftTracker Mail System " - send_mail(request, to, frm, - "IESG Deferred Ballot notification: %s" % doc.file_tag(), - "doc/mail/ballot_deferred_email.txt", - dict(doc=doc, - by=by, - action='deferred', - telechat_date=telechat_date), - cc=cc) + send_mail( + request, + to, + frm, + "IESG Deferred Ballot notification: %s" % doc.file_tag(), + "doc/mail/ballot_deferred_email.txt", + dict(doc=doc, by=by, action="deferred", telechat_date=telechat_date), + cc=cc, + ) + def email_ballot_undeferred(request, doc, by, telechat_date): - (to, cc) = gather_address_lists('ballot_deferred',doc=doc) + (to, cc) = gather_address_lists("ballot_deferred", doc=doc) frm = "DraftTracker Mail System " - send_mail(request, to, frm, - "IESG Undeferred Ballot notification: %s" % doc.file_tag(), - "doc/mail/ballot_deferred_email.txt", - dict(doc=doc, - by=by, - action='undeferred', - telechat_date=telechat_date), - cc=cc) + send_mail( + request, + to, + frm, + "IESG Undeferred Ballot notification: %s" % doc.file_tag(), + "doc/mail/ballot_deferred_email.txt", + dict(doc=doc, by=by, action="undeferred", telechat_date=telechat_date), + cc=cc, + ) + def generate_issue_ballot_mail(request, doc, ballot): - + e = doc.latest_event(LastCallDocEvent, type="sent_last_call") last_call_expires = e.expires if e else None last_call_has_expired = last_call_expires and last_call_expires < timezone.now() - return render_to_string("doc/mail/issue_iesg_ballot_mail.txt", - dict(doc=doc, - doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), - last_call_expires=last_call_expires, - last_call_has_expired=last_call_has_expired, - needed_ballot_positions= - needed_ballot_positions(doc, - list(doc.active_ballot().active_balloter_positions().values()) - ), - ) - ) + return render_to_string( + "doc/mail/issue_iesg_ballot_mail.txt", + dict( + doc=doc, + doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), + last_call_expires=last_call_expires, + last_call_has_expired=last_call_has_expired, + needed_ballot_positions=needed_ballot_positions( + doc, list(doc.active_ballot().active_balloter_positions().values()) + ), + ), + ) + def _send_irsg_ballot_email(request, doc, ballot, subject, template): """Send email notification when IRSG ballot is issued""" - (to, cc) = gather_address_lists('irsg_ballot_issued', doc=doc) - sender = 'IESG Secretary ' + (to, cc) = gather_address_lists("irsg_ballot_issued", doc=doc) + sender = "IESG Secretary " ballot_expired = ballot.duedate < timezone.now() active_ballot = doc.active_ballot() if active_ballot is None: - needed_bps = '' + needed_bps = "" else: needed_bps = needed_ballot_positions( - doc, - list(active_ballot.active_balloter_positions().values()) + doc, list(active_ballot.active_balloter_positions().values()) ) return send_mail( @@ -458,7 +574,7 @@ def _send_irsg_ballot_email(request, doc, ballot, subject, template): to=to, cc=cc, subject=subject, - extra={'Reply-To': [sender]}, + extra={"Reply-To": [sender]}, template=template, context=dict( doc=doc, @@ -466,7 +582,8 @@ def _send_irsg_ballot_email(request, doc, ballot, subject, template): ballot_duedate=ballot.duedate, ballot_expired=ballot_expired, needed_ballot_positions=needed_bps, - )) + ), + ) def email_irsg_ballot_issued(request, doc, ballot): @@ -475,32 +592,33 @@ def email_irsg_ballot_issued(request, doc, ballot): request, doc, ballot, - 'IRSG ballot issued: %s to %s'%(doc.file_tag(), std_level_prompt(doc)), - 'doc/mail/issue_irsg_ballot_mail.txt', + "IRSG ballot issued: %s to %s" % (doc.file_tag(), std_level_prompt(doc)), + "doc/mail/issue_irsg_ballot_mail.txt", ) + def email_irsg_ballot_closed(request, doc, ballot): """Send email notification when IRSG ballot is closed""" return _send_irsg_ballot_email( request, doc, ballot, - 'IRSG ballot closed: %s to %s'%(doc.file_tag(), std_level_prompt(doc)), + "IRSG ballot closed: %s to %s" % (doc.file_tag(), std_level_prompt(doc)), "doc/mail/close_irsg_ballot_mail.txt", ) + def _send_rsab_ballot_email(request, doc, ballot, subject, template): """Send email notification when IRSG ballot is issued""" - (to, cc) = gather_address_lists('rsab_ballot_issued', doc=doc) - sender = 'IESG Secretary ' + (to, cc) = gather_address_lists("rsab_ballot_issued", doc=doc) + sender = "IESG Secretary " active_ballot = doc.active_ballot() if active_ballot is None: - needed_bps = '' + needed_bps = "" else: needed_bps = needed_ballot_positions( - doc, - list(active_ballot.active_balloter_positions().values()) + doc, list(active_ballot.active_balloter_positions().values()) ) return send_mail( @@ -509,13 +627,15 @@ def _send_rsab_ballot_email(request, doc, ballot, subject, template): to=to, cc=cc, subject=subject, - extra={'Reply-To': [sender]}, + extra={"Reply-To": [sender]}, template=template, context=dict( doc=doc, doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), needed_ballot_positions=needed_bps, - )) + ), + ) + def email_rsab_ballot_issued(request, doc, ballot): """Send email notification when RSAB ballot is issued""" @@ -523,263 +643,407 @@ def email_rsab_ballot_issued(request, doc, ballot): request, doc, ballot, - 'RSAB ballot issued: %s to %s'%(doc.file_tag(), std_level_prompt(doc)), - 'doc/mail/issue_rsab_ballot_mail.txt', + "RSAB ballot issued: %s to %s" % (doc.file_tag(), std_level_prompt(doc)), + "doc/mail/issue_rsab_ballot_mail.txt", ) + def email_rsab_ballot_closed(request, doc, ballot): """Send email notification when RSAB ballot is closed""" return _send_rsab_ballot_email( request, doc, ballot, - 'RSAB ballot closed: %s to %s'%(doc.file_tag(), std_level_prompt(doc)), + "RSAB ballot closed: %s to %s" % (doc.file_tag(), std_level_prompt(doc)), "doc/mail/close_rsab_ballot_mail.txt", ) + def email_iana(request, doc, to, msg, cc=None): # fix up message and send it with extra info on doc in headers import email + parsed_msg = email.message_from_string(msg) - parsed_msg.set_charset('UTF-8') + parsed_msg.set_charset("UTF-8") extra = extra_automation_headers(doc) - extra["Reply-To"] = ["noreply@ietf.org", ] - - send_mail_text(request, to, - parsed_msg["From"], parsed_msg["Subject"], - parsed_msg.get_payload(), - extra=extra, - cc=cc) + extra["Reply-To"] = [ + "noreply@ietf.org", + ] + + send_mail_text( + request, + to, + parsed_msg["From"], + parsed_msg["Subject"], + parsed_msg.get_payload(), + extra=extra, + cc=cc, + ) + def extra_automation_headers(doc): extra = {} - extra["X-IETF-Draft-string"] = [ doc.name, ] - extra["X-IETF-Draft-revision"] = [ doc.rev, ] + extra["X-IETF-Draft-string"] = [ + doc.name, + ] + extra["X-IETF-Draft-revision"] = [ + doc.rev, + ] return extra + def email_last_call_expired(doc): - if not doc.type_id in ['draft','statchg']: + if not doc.type_id in ["draft", "statchg"]: return - text = "IETF Last Call has ended, and the state has been changed to\n%s." % doc.get_state("draft-iesg" if doc.type_id == 'draft' else "statchg").name - addrs = gather_address_lists('last_call_expired',doc=doc) - - send_mail(None, - addrs.to, - "DraftTracker Mail System ", - "IETF Last Call Expired: %s" % doc.file_tag(), - "doc/mail/change_notice.txt", - dict(text=text, - doc=doc, - url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), - cc = addrs.cc) + text = ( + "IETF Last Call has ended, and the state has been changed to\n%s." + % doc.get_state("draft-iesg" if doc.type_id == "draft" else "statchg").name + ) + addrs = gather_address_lists("last_call_expired", doc=doc) + + send_mail( + None, + addrs.to, + "DraftTracker Mail System ", + "IETF Last Call Expired: %s" % doc.file_tag(), + "doc/mail/change_notice.txt", + dict( + text=text, doc=doc, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url() + ), + cc=addrs.cc, + ) + def email_last_call_expired_with_downref(doc, last_call_text): - if doc.type_id != 'draft': + if doc.type_id != "draft": return - send_mail(None, - (doc.ad.email().address, ), - "DraftTracker Mail System ", - "Review Downrefs From Expired Last Call: %s" % doc.file_tag(), - "doc/mail/downrefs_notice.txt", - dict(last_call_text=last_call_text, - doc=doc, - url=settings.IDTRACKER_BASE_URL + "/downref/add/")) + send_mail( + None, + (doc.ad.email().address,), + "DraftTracker Mail System ", + "Review Downrefs From Expired Last Call: %s" % doc.file_tag(), + "doc/mail/downrefs_notice.txt", + dict( + last_call_text=last_call_text, + doc=doc, + url=settings.IDTRACKER_BASE_URL + "/downref/add/", + ), + ) + def email_intended_status_changed(request, doc, text): - (to,cc) = gather_address_lists('doc_intended_status_changed',doc=doc) + (to, cc) = gather_address_lists("doc_intended_status_changed", doc=doc) if not to: return - + text = strip_tags(text) - send_mail(request, to, None, - "Intended Status for %s changed to %s" % (doc.file_tag(),doc.intended_std_level), - "doc/mail/intended_status_changed_email.txt", - dict(text=text, - url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), - cc=cc) + send_mail( + request, + to, + None, + "Intended Status for %s changed to %s" + % (doc.file_tag(), doc.intended_std_level), + "doc/mail/intended_status_changed_email.txt", + dict(text=text, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), + cc=cc, + ) + def email_comment(request, doc, comment): - (to, cc) = gather_address_lists('doc_added_comment',doc=doc) + (to, cc) = gather_address_lists("doc_added_comment", doc=doc) - send_mail(request, to, None, "Comment added to %s history"%doc.name, - "doc/mail/comment_added_email.txt", - dict( - comment=comment, - doc=doc, - by=request.user.person, - url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), - ), - cc = cc) + send_mail( + request, + to, + None, + "Comment added to %s history" % doc.name, + "doc/mail/comment_added_email.txt", + dict( + comment=comment, + doc=doc, + by=request.user.person, + url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), + ), + cc=cc, + ) def email_adopted(request, doc, prev_state, new_state, by, comment=""): - (to, cc) = gather_address_lists('doc_adopted_by_group',doc=doc) + (to, cc) = gather_address_lists("doc_adopted_by_group", doc=doc) state_type = (prev_state or new_state).type - send_mail(request, to, settings.DEFAULT_FROM_EMAIL, - 'The %s %s has placed %s in state "%s"' % - (doc.group.acronym.upper(),doc.group.type_id.upper(), doc.name, new_state or "None"), - 'doc/mail/doc_adopted_email.txt', - dict(doc=doc, - url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), - state_type=state_type, - prev_state=prev_state, - new_state=new_state, - by=by, - comment=comment), - cc=cc) + send_mail( + request, + to, + settings.DEFAULT_FROM_EMAIL, + 'The %s %s has placed %s in state "%s"' + % ( + doc.group.acronym.upper(), + doc.group.type_id.upper(), + doc.name, + new_state or "None", + ), + "doc/mail/doc_adopted_email.txt", + dict( + doc=doc, + url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), + state_type=state_type, + prev_state=prev_state, + new_state=new_state, + by=by, + comment=comment, + ), + cc=cc, + ) + def email_stream_state_changed(request, doc, prev_state, new_state, by, comment=""): - (to, cc)= gather_address_lists('doc_stream_state_edited',doc=doc) + (to, cc) = gather_address_lists("doc_stream_state_edited", doc=doc) state_type = (prev_state or new_state).type - send_mail(request, to, settings.DEFAULT_FROM_EMAIL, - "%s changed for %s" % (state_type.label, doc.name), - 'doc/mail/stream_state_changed_email.txt', - dict(doc=doc, - url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), - state_type=state_type, - prev_state=prev_state, - new_state=new_state, - by=by, - comment=comment), - cc=cc) + send_mail( + request, + to, + settings.DEFAULT_FROM_EMAIL, + "%s changed for %s" % (state_type.label, doc.name), + "doc/mail/stream_state_changed_email.txt", + dict( + doc=doc, + url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), + state_type=state_type, + prev_state=prev_state, + new_state=new_state, + by=by, + comment=comment, + ), + cc=cc, + ) + def email_stream_tags_changed(request, doc, added_tags, removed_tags, by, comment=""): - (to, cc) = gather_address_lists('doc_stream_state_edited',doc=doc) + (to, cc) = gather_address_lists("doc_stream_state_edited", doc=doc) + + send_mail( + request, + to, + settings.DEFAULT_FROM_EMAIL, + "Tags changed for %s" % doc.name, + "doc/mail/stream_tags_changed_email.txt", + dict( + doc=doc, + url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), + added=added_tags, + removed=removed_tags, + by=by, + comment=comment, + ), + cc=cc, + ) - send_mail(request, to, settings.DEFAULT_FROM_EMAIL, - "Tags changed for %s" % doc.name, - 'doc/mail/stream_tags_changed_email.txt', - dict(doc=doc, - url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), - added=added_tags, - removed=removed_tags, - by=by, - comment=comment), - cc=cc) def send_review_possibly_replaces_request(request, doc, submitter_info): - addrs = gather_address_lists('doc_replacement_suggested',doc=doc) + addrs = gather_address_lists("doc_replacement_suggested", doc=doc) to = set(addrs.to) cc = set(addrs.cc) - possibly_replaces = Document.objects.filter(name__in=[related.name for related in doc.related_that_doc("possibly-replaces")]) + possibly_replaces = Document.objects.filter( + name__in=[related.name for related in doc.related_that_doc("possibly-replaces")] + ) for other_doc in possibly_replaces: - (other_to, other_cc) = gather_address_lists('doc_replacement_suggested',doc=other_doc) + (other_to, other_cc) = gather_address_lists( + "doc_replacement_suggested", doc=other_doc + ) to.update(other_to) cc.update(other_cc) - send_mail(request, list(to), settings.DEFAULT_FROM_EMAIL, - 'Review of suggested possible replacements for %s-%s needed' % (doc.name, doc.rev), - 'doc/mail/review_possibly_replaces_request.txt', - dict(doc= doc, - submitter_info=submitter_info, - possibly_replaces=doc.related_that_doc("possibly-replaces"), - review_url=settings.IDTRACKER_BASE_URL + urlreverse('ietf.doc.views_draft.review_possibly_replaces', kwargs={ "name": doc.name })), - cc=list(cc),) + send_mail( + request, + list(to), + settings.DEFAULT_FROM_EMAIL, + "Review of suggested possible replacements for %s-%s needed" + % (doc.name, doc.rev), + "doc/mail/review_possibly_replaces_request.txt", + dict( + doc=doc, + submitter_info=submitter_info, + possibly_replaces=doc.related_that_doc("possibly-replaces"), + review_url=settings.IDTRACKER_BASE_URL + + urlreverse( + "ietf.doc.views_draft.review_possibly_replaces", + kwargs={"name": doc.name}, + ), + ), + cc=list(cc), + ) + def email_charter_internal_review(request, charter): - addrs = gather_address_lists('charter_internal_review',doc=charter,group=charter.group) - charter_text = charter.text_or_error() # pyflakes:ignore - - send_mail(request, addrs.to, settings.DEFAULT_FROM_EMAIL, - 'Internal %s Review: %s (%s)'%(charter.group.type.name,charter.group.name,charter.group.acronym), - 'doc/mail/charter_internal_review.txt', - dict(charter=charter, - charter_text=charter_text, - review_type = "new" if charter.group.state_id == "proposed" else "recharter", - charter_url=settings.IDTRACKER_BASE_URL + charter.get_absolute_url(), - chairs=charter.group.role_set.filter(name="chair"), - secr=charter.group.role_set.filter(name="secr"), - ads=charter.group.role_set.filter(name='ad'), - parent_ads=charter.group.parent.role_set.filter(name='ad'), - techadv=charter.group.role_set.filter(name="techadv"), - milestones=charter.group.groupmilestone_set.filter(state="charter"), - ), - cc=addrs.cc, - extra={'Reply-To': ["irsg@irtf.org" if charter.group.type_id == 'rg' else "iesg@ietf.org", ]}, - ) + addrs = gather_address_lists( + "charter_internal_review", doc=charter, group=charter.group + ) + charter_text = charter.text_or_error() # pyflakes:ignore + + send_mail( + request, + addrs.to, + settings.DEFAULT_FROM_EMAIL, + "Internal %s Review: %s (%s)" + % (charter.group.type.name, charter.group.name, charter.group.acronym), + "doc/mail/charter_internal_review.txt", + dict( + charter=charter, + charter_text=charter_text, + review_type="new" if charter.group.state_id == "proposed" else "recharter", + charter_url=settings.IDTRACKER_BASE_URL + charter.get_absolute_url(), + chairs=charter.group.role_set.filter(name="chair"), + secr=charter.group.role_set.filter(name="secr"), + ads=charter.group.role_set.filter(name="ad"), + parent_ads=charter.group.parent.role_set.filter(name="ad"), + techadv=charter.group.role_set.filter(name="techadv"), + milestones=charter.group.groupmilestone_set.filter(state="charter"), + ), + cc=addrs.cc, + extra={ + "Reply-To": [ + "irsg@irtf.org" if charter.group.type_id == "rg" else "iesg@ietf.org", + ] + }, + ) + def email_lc_to_yang_doctors(request, doc): - addrs = gather_address_lists('last_call_of_doc_with_yang_issued') - send_mail(request, addrs.to, settings.DEFAULT_FROM_EMAIL, - 'Attn YangDoctors: IETF LC issued for %s' % doc.name , - 'doc/mail/lc_to_yang_doctors.txt', - dict(doc=doc, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url() ), - cc = addrs.cc, - ) + addrs = gather_address_lists("last_call_of_doc_with_yang_issued") + send_mail( + request, + addrs.to, + settings.DEFAULT_FROM_EMAIL, + "Attn YangDoctors: IETF LC issued for %s" % doc.name, + "doc/mail/lc_to_yang_doctors.txt", + dict(doc=doc, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), + cc=addrs.cc, + ) + def email_iana_expert_review_state_changed(request, events): assert type(events) == list assert len(events) == 1 - addrs = gather_address_lists('iana_expert_review_state_changed', doc=events[0].doc) - send_mail(request, addrs.to, settings.DEFAULT_FROM_EMAIL, - f'IANA expert review state changed to {events[0].state.name} for {events[0].doc.name}', - 'doc/mail/iana_expert_review_state_changed.txt', - dict(event=events[0], url=settings.IDTRACKER_BASE_URL + events[0].doc.get_absolute_url() ), - cc = addrs.cc, - ) - -def send_external_resource_change_request(request, doc, submitter_info, requested_resources): + addrs = gather_address_lists("iana_expert_review_state_changed", doc=events[0].doc) + send_mail( + request, + addrs.to, + settings.DEFAULT_FROM_EMAIL, + f"IANA expert review state changed to {events[0].state.name} for {events[0].doc.name}", + "doc/mail/iana_expert_review_state_changed.txt", + dict( + event=events[0], + url=settings.IDTRACKER_BASE_URL + events[0].doc.get_absolute_url(), + ), + cc=addrs.cc, + ) + + +def send_external_resource_change_request( + request, doc, submitter_info, requested_resources +): """Send an email to requesting changes to a draft's external resources""" - addrs = gather_address_lists('doc_external_resource_change_requested', doc=doc) + addrs = gather_address_lists("doc_external_resource_change_requested", doc=doc) to = set(addrs.to) cc = set(addrs.cc) - send_mail(request, list(to), settings.DEFAULT_FROM_EMAIL, - 'External resource change requested for %s' % doc.name, - 'doc/mail/external_resource_change_request.txt', - dict( - doc=doc, - submitter_info=submitter_info, - requested_resources=requested_resources, - doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), - ), - cc=list(cc),) + send_mail( + request, + list(to), + settings.DEFAULT_FROM_EMAIL, + "External resource change requested for %s" % doc.name, + "doc/mail/external_resource_change_request.txt", + dict( + doc=doc, + submitter_info=submitter_info, + requested_resources=requested_resources, + doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), + ), + cc=list(cc), + ) + def email_bofreq_title_changed(request, bofreq): - addrs = gather_address_lists('bofreq_title_changed', doc=bofreq) + addrs = gather_address_lists("bofreq_title_changed", doc=bofreq) + + send_mail( + request, + addrs.to, + settings.DEFAULT_FROM_EMAIL, + f"BOF Request title changed : {bofreq.name}", + "doc/mail/bofreq_title_changed.txt", + dict(bofreq=bofreq, request=request), + cc=addrs.cc, + ) - send_mail(request, addrs.to, settings.DEFAULT_FROM_EMAIL, - f'BOF Request title changed : {bofreq.name}', - 'doc/mail/bofreq_title_changed.txt', - dict(bofreq=bofreq, request=request), - cc=addrs.cc) def plain_names(persons): return [p.plain_name() for p in persons] + def email_bofreq_editors_changed(request, bofreq, previous_editors): editors = bofreq_editors(bofreq) - addrs = gather_address_lists('bofreq_editors_changed', doc=bofreq, previous_editors=previous_editors) + addrs = gather_address_lists( + "bofreq_editors_changed", doc=bofreq, previous_editors=previous_editors + ) + + send_mail( + request, + addrs.to, + settings.DEFAULT_FROM_EMAIL, + f"BOF Request editors changed : {bofreq.name}", + "doc/mail/bofreq_editors_changed.txt", + dict( + bofreq=bofreq, + request=request, + editors=plain_names(editors), + previous_editors=plain_names(previous_editors), + ), + cc=addrs.cc, + ) - send_mail(request, addrs.to, settings.DEFAULT_FROM_EMAIL, - f'BOF Request editors changed : {bofreq.name}', - 'doc/mail/bofreq_editors_changed.txt', - dict(bofreq=bofreq, request=request, editors=plain_names(editors), previous_editors=plain_names(previous_editors)), - cc=addrs.cc) def email_bofreq_responsible_changed(request, bofreq, previous_responsible): responsible = bofreq_responsible(bofreq) - addrs = gather_address_lists('bofreq_responsible_changed', doc=bofreq, previous_responsible=previous_responsible) + addrs = gather_address_lists( + "bofreq_responsible_changed", + doc=bofreq, + previous_responsible=previous_responsible, + ) + + send_mail( + request, + addrs.to, + settings.DEFAULT_FROM_EMAIL, + f"BOF Request responsible leadership changed : {bofreq.name}", + "doc/mail/bofreq_responsible_changed.txt", + dict( + bofreq=bofreq, + request=request, + responsible=plain_names(responsible), + previous_responsible=plain_names(previous_responsible), + ), + cc=addrs.cc, + ) - send_mail(request, addrs.to, settings.DEFAULT_FROM_EMAIL, - f'BOF Request responsible leadership changed : {bofreq.name}', - 'doc/mail/bofreq_responsible_changed.txt', - dict(bofreq=bofreq, request=request, responsible=plain_names(responsible), previous_responsible=plain_names(previous_responsible)), - cc=addrs.cc) def email_bofreq_new_revision(request, bofreq): - addrs = gather_address_lists('bofreq_new_revision', doc=bofreq) - send_mail(request, addrs.to, settings.DEFAULT_FROM_EMAIL, - f'New BOF request revision uploaded: {bofreq.name}-{bofreq.rev}', - 'doc/mail/bofreq_new_revision.txt', - dict(bofreq=bofreq, request=request ), - cc=addrs.cc) + addrs = gather_address_lists("bofreq_new_revision", doc=bofreq) + send_mail( + request, + addrs.to, + settings.DEFAULT_FROM_EMAIL, + f"New BOF request revision uploaded: {bofreq.name}-{bofreq.rev}", + "doc/mail/bofreq_new_revision.txt", + dict(bofreq=bofreq, request=request), + cc=addrs.cc, + ) diff --git a/ietf/doc/management/commands/find_github_backup_info.py b/ietf/doc/management/commands/find_github_backup_info.py index f1f71452df..d63e088119 100644 --- a/ietf/doc/management/commands/find_github_backup_info.py +++ b/ietf/doc/management/commands/find_github_backup_info.py @@ -13,28 +13,39 @@ from ietf.group.models import GroupExtResource from ietf.person.models import PersonExtResource -# TODO: Think more about submodules. This currently will only take top level repos, with the assumption that the clone will include arguments to grab all the submodules. +# TODO: Think more about submodules. This currently will only take top level repos, with the assumption that the clone will include arguments to grab all the submodules. # As a consequence, we might end up pulling more than we need (or that the org or user expected) # Make sure this is what we want. + class Command(BaseCommand): - help = ('Locate information about github repositories to backup') + help = "Locate information about github repositories to backup" def add_arguments(self, parser): - parser.add_argument('--verbose', dest='verbose', action='store_true', help='Show counts of types of repositories') + parser.add_argument( + "--verbose", + dest="verbose", + action="store_true", + help="Show counts of types of repositories", + ) def handle(self, *args, **options): - if not (hasattr(settings,'GITHUB_BACKUP_API_KEY') and settings.GITHUB_BACKUP_API_KEY): - raise CommandError("ERROR: can't find GITHUB_BACKUP_API_KEY") # TODO: at >= py3.1, use returncode + if not ( + hasattr(settings, "GITHUB_BACKUP_API_KEY") + and settings.GITHUB_BACKUP_API_KEY + ): + raise CommandError( + "ERROR: can't find GITHUB_BACKUP_API_KEY" + ) # TODO: at >= py3.1, use returncode - github = github3.login(token = settings.GITHUB_BACKUP_API_KEY) + github = github3.login(token=settings.GITHUB_BACKUP_API_KEY) owners = dict() repos = set() for cls in (DocExtResource, GroupExtResource, PersonExtResource): - for res in cls.objects.filter(name_id__in=('github_repo','github_org')): - path_parts = urlparse(res.value).path.strip('/').split('/') + for res in cls.objects.filter(name_id__in=("github_repo", "github_org")): + path_parts = urlparse(res.value).path.strip("/").split("/") if not path_parts or not path_parts[0]: continue @@ -45,30 +56,29 @@ def handle(self, *args, **options): gh_owner = github.user(username=owner) owners[owner] = gh_owner except github3.exceptions.NotFoundError: - continue + continue - if gh_owner.type in ('User', 'Organization'): + if gh_owner.type in ("User", "Organization"): if len(path_parts) > 1: repo = path_parts[1] if (owner, repo) not in repos: try: - github.repository(owner,repo) - repos.add( (owner, repo) ) + github.repository(owner, repo) + repos.add((owner, repo)) except github3.exceptions.NotFoundError: continue else: for repo in github.repositories_by(owner): - repos.add( (owner, repo.name) ) + repos.add((owner, repo.name)) owner_types = Counter([owners[owner].type for owner in owners]) - if options['verbose']: + if options["verbose"]: self.stdout.write("Owners:") for key in owner_types: - self.stdout.write(" %s: %s"%(key,owner_types[key])) + self.stdout.write(" %s: %s" % (key, owner_types[key])) self.stdout.write("Repositories: %d" % len(repos)) for repo in sorted(repos): - self.stdout.write(" https://github.com/%s/%s" % repo ) + self.stdout.write(" https://github.com/%s/%s" % repo) else: for repo in sorted(repos): - self.stdout.write("%s/%s" % repo ) - + self.stdout.write("%s/%s" % repo) diff --git a/ietf/doc/management/commands/reset_rfc_authors.py b/ietf/doc/management/commands/reset_rfc_authors.py index e2ab5f1208..7b433d9b7a 100644 --- a/ietf/doc/management/commands/reset_rfc_authors.py +++ b/ietf/doc/management/commands/reset_rfc_authors.py @@ -35,7 +35,9 @@ def handle(self, *args, **options): raise CommandError( f"{rfc.name} already has authors. Not resetting. Use '--force' to reset anyway." ) - removed_auth_names = list(orig_authors.values_list("person__name", flat=True)) + removed_auth_names = list( + orig_authors.values_list("person__name", flat=True) + ) rfc.documentauthor_set.all().delete() DocEvent.objects.create( doc=rfc, @@ -58,7 +60,9 @@ def handle(self, *args, **options): author.document = rfc author.save() self.stdout.write( - self.style.SUCCESS(f"Added author {author.person.name} <{author.email}>") + self.style.SUCCESS( + f"Added author {author.person.name} <{author.email}>" + ) ) auth_names = draft.documentauthor_set.values_list("person__name", flat=True) DocEvent.objects.create( diff --git a/ietf/doc/management/commands/tests.py b/ietf/doc/management/commands/tests.py index 8244d87266..bbd8f154ce 100644 --- a/ietf/doc/management/commands/tests.py +++ b/ietf/doc/management/commands/tests.py @@ -47,7 +47,9 @@ def test_reset_rfc_authors(self): with self.assertRaises(CommandError, msg="Cannot reset an RFC with no draft"): self._call_command(command_name, rfc.rfc_number) - with self.assertRaises(CommandError, msg="Cannot force-reset an RFC with no draft"): + with self.assertRaises( + CommandError, msg="Cannot force-reset an RFC with no draft" + ): self._call_command(command_name, rfc.rfc_number, "--force") # Link the draft to the rfc diff --git a/ietf/doc/migrations/0001_initial.py b/ietf/doc/migrations/0001_initial.py index 2823abfe63..26b71f99fc 100644 --- a/ietf/doc/migrations/0001_initial.py +++ b/ietf/doc/migrations/0001_initial.py @@ -12,357 +12,961 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('name', '0001_initial'), - ('person', '0001_initial'), + ("name", "0001_initial"), + ("person", "0001_initial"), ] operations = [ migrations.CreateModel( - name='BallotType', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('slug', models.SlugField()), - ('name', models.CharField(max_length=255)), - ('question', models.TextField(blank=True)), - ('used', models.BooleanField(default=True)), - ('order', models.IntegerField(default=0)), + name="BallotType", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("slug", models.SlugField()), + ("name", models.CharField(max_length=255)), + ("question", models.TextField(blank=True)), + ("used", models.BooleanField(default=True)), + ("order", models.IntegerField(default=0)), ], options={ - 'ordering': ['order'], + "ordering": ["order"], }, ), migrations.CreateModel( - name='DeletedEvent', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('json', models.TextField(help_text='Deleted object in JSON format, with attribute names chosen to be suitable for passing into the relevant create method.')), - ('time', models.DateTimeField(default=django.utils.timezone.now)), - ], - ), - migrations.CreateModel( - name='DocAlias', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255, unique=True)), + name="DeletedEvent", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "json", + models.TextField( + help_text="Deleted object in JSON format, with attribute names chosen to be suitable for passing into the relevant create method." + ), + ), + ("time", models.DateTimeField(default=django.utils.timezone.now)), + ], + ), + migrations.CreateModel( + name="DocAlias", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255, unique=True)), ], options={ - 'verbose_name': 'document alias', - 'verbose_name_plural': 'document aliases', + "verbose_name": "document alias", + "verbose_name_plural": "document aliases", }, ), migrations.CreateModel( - name='DocEvent', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('time', models.DateTimeField(db_index=True, default=django.utils.timezone.now, help_text='When the event happened')), - ('type', models.CharField(choices=[('new_revision', 'Added new revision'), ('new_submission', 'Uploaded new revision'), ('changed_document', 'Changed document metadata'), ('added_comment', 'Added comment'), ('added_message', 'Added message'), ('edited_authors', 'Edited the documents author list'), ('deleted', 'Deleted document'), ('changed_state', 'Changed state'), ('changed_stream', 'Changed document stream'), ('expired_document', 'Expired document'), ('extended_expiry', 'Extended expiry of document'), ('requested_resurrect', 'Requested resurrect'), ('completed_resurrect', 'Completed resurrect'), ('changed_consensus', 'Changed consensus'), ('published_rfc', 'Published RFC'), ('added_suggested_replaces', 'Added suggested replacement relationships'), ('reviewed_suggested_replaces', 'Reviewed suggested replacement relationships'), ('changed_action_holders', 'Changed action holders for document'), ('changed_group', 'Changed group'), ('changed_protocol_writeup', 'Changed protocol writeup'), ('changed_charter_milestone', 'Changed charter milestone'), ('initial_review', 'Set initial review time'), ('changed_review_announcement', 'Changed WG Review text'), ('changed_action_announcement', 'Changed WG Action text'), ('started_iesg_process', 'Started IESG process on document'), ('created_ballot', 'Created ballot'), ('closed_ballot', 'Closed ballot'), ('sent_ballot_announcement', 'Sent ballot announcement'), ('changed_ballot_position', 'Changed ballot position'), ('changed_ballot_approval_text', 'Changed ballot approval text'), ('changed_ballot_writeup_text', 'Changed ballot writeup text'), ('changed_rfc_editor_note_text', 'Changed RFC Editor Note text'), ('changed_last_call_text', 'Changed last call text'), ('requested_last_call', 'Requested last call'), ('sent_last_call', 'Sent last call'), ('scheduled_for_telechat', 'Scheduled for telechat'), ('iesg_approved', 'IESG approved document (no problem)'), ('iesg_disapproved', 'IESG disapproved document (do not publish)'), ('approved_in_minute', 'Approved in minute'), ('iana_review', 'IANA review comment'), ('rfc_in_iana_registry', 'RFC is in IANA registry'), ('rfc_editor_received_announcement', 'Announcement was received by RFC Editor'), ('requested_publication', 'Publication at RFC Editor requested'), ('sync_from_rfc_editor', 'Received updated information from RFC Editor'), ('requested_review', 'Requested review'), ('assigned_review_request', 'Assigned review request'), ('closed_review_request', 'Closed review request'), ('closed_review_assignment', 'Closed review assignment'), ('downref_approved', 'Downref approved'), ('posted_related_ipr', 'Posted related IPR'), ('removed_related_ipr', 'Removed related IPR'), ('changed_editors', 'Changed BOF Request editors')], max_length=50)), - ('rev', models.CharField(blank=True, max_length=16, null=True, verbose_name='revision')), - ('desc', models.TextField()), + name="DocEvent", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "time", + models.DateTimeField( + db_index=True, + default=django.utils.timezone.now, + help_text="When the event happened", + ), + ), + ( + "type", + models.CharField( + choices=[ + ("new_revision", "Added new revision"), + ("new_submission", "Uploaded new revision"), + ("changed_document", "Changed document metadata"), + ("added_comment", "Added comment"), + ("added_message", "Added message"), + ("edited_authors", "Edited the documents author list"), + ("deleted", "Deleted document"), + ("changed_state", "Changed state"), + ("changed_stream", "Changed document stream"), + ("expired_document", "Expired document"), + ("extended_expiry", "Extended expiry of document"), + ("requested_resurrect", "Requested resurrect"), + ("completed_resurrect", "Completed resurrect"), + ("changed_consensus", "Changed consensus"), + ("published_rfc", "Published RFC"), + ( + "added_suggested_replaces", + "Added suggested replacement relationships", + ), + ( + "reviewed_suggested_replaces", + "Reviewed suggested replacement relationships", + ), + ( + "changed_action_holders", + "Changed action holders for document", + ), + ("changed_group", "Changed group"), + ("changed_protocol_writeup", "Changed protocol writeup"), + ("changed_charter_milestone", "Changed charter milestone"), + ("initial_review", "Set initial review time"), + ("changed_review_announcement", "Changed WG Review text"), + ("changed_action_announcement", "Changed WG Action text"), + ( + "started_iesg_process", + "Started IESG process on document", + ), + ("created_ballot", "Created ballot"), + ("closed_ballot", "Closed ballot"), + ("sent_ballot_announcement", "Sent ballot announcement"), + ("changed_ballot_position", "Changed ballot position"), + ( + "changed_ballot_approval_text", + "Changed ballot approval text", + ), + ( + "changed_ballot_writeup_text", + "Changed ballot writeup text", + ), + ( + "changed_rfc_editor_note_text", + "Changed RFC Editor Note text", + ), + ("changed_last_call_text", "Changed last call text"), + ("requested_last_call", "Requested last call"), + ("sent_last_call", "Sent last call"), + ("scheduled_for_telechat", "Scheduled for telechat"), + ("iesg_approved", "IESG approved document (no problem)"), + ( + "iesg_disapproved", + "IESG disapproved document (do not publish)", + ), + ("approved_in_minute", "Approved in minute"), + ("iana_review", "IANA review comment"), + ("rfc_in_iana_registry", "RFC is in IANA registry"), + ( + "rfc_editor_received_announcement", + "Announcement was received by RFC Editor", + ), + ( + "requested_publication", + "Publication at RFC Editor requested", + ), + ( + "sync_from_rfc_editor", + "Received updated information from RFC Editor", + ), + ("requested_review", "Requested review"), + ("assigned_review_request", "Assigned review request"), + ("closed_review_request", "Closed review request"), + ("closed_review_assignment", "Closed review assignment"), + ("downref_approved", "Downref approved"), + ("posted_related_ipr", "Posted related IPR"), + ("removed_related_ipr", "Removed related IPR"), + ("changed_editors", "Changed BOF Request editors"), + ], + max_length=50, + ), + ), + ( + "rev", + models.CharField( + blank=True, max_length=16, null=True, verbose_name="revision" + ), + ), + ("desc", models.TextField()), ], options={ - 'ordering': ['-time', '-id'], + "ordering": ["-time", "-id"], }, ), migrations.CreateModel( - name='DocExtResource', + name="DocExtResource", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('display_name', models.CharField(blank=True, default='', max_length=255)), - ('value', models.CharField(max_length=2083)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "display_name", + models.CharField(blank=True, default="", max_length=255), + ), + ("value", models.CharField(max_length=2083)), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.CreateModel( - name='DocHistory', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('time', models.DateTimeField(default=django.utils.timezone.now)), - ('title', models.CharField(max_length=255, validators=[django.core.validators.RegexValidator(message='Please enter a string without control characters.', regex='^[^\x00-\x1f]*$')])), - ('abstract', models.TextField(blank=True)), - ('rev', models.CharField(blank=True, max_length=16, verbose_name='revision')), - ('pages', models.IntegerField(blank=True, null=True)), - ('words', models.IntegerField(blank=True, null=True)), - ('order', models.IntegerField(blank=True, default=1)), - ('expires', models.DateTimeField(blank=True, null=True)), - ('notify', models.TextField(blank=True, max_length=1023)), - ('external_url', models.URLField(blank=True)), - ('uploaded_filename', models.TextField(blank=True)), - ('note', models.TextField(blank=True)), - ('internal_comments', models.TextField(blank=True)), - ('name', models.CharField(max_length=255)), + name="DocHistory", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("time", models.DateTimeField(default=django.utils.timezone.now)), + ( + "title", + models.CharField( + max_length=255, + validators=[ + django.core.validators.RegexValidator( + message="Please enter a string without control characters.", + regex="^[^\x00-\x1f]*$", + ) + ], + ), + ), + ("abstract", models.TextField(blank=True)), + ( + "rev", + models.CharField( + blank=True, max_length=16, verbose_name="revision" + ), + ), + ("pages", models.IntegerField(blank=True, null=True)), + ("words", models.IntegerField(blank=True, null=True)), + ("order", models.IntegerField(blank=True, default=1)), + ("expires", models.DateTimeField(blank=True, null=True)), + ("notify", models.TextField(blank=True, max_length=1023)), + ("external_url", models.URLField(blank=True)), + ("uploaded_filename", models.TextField(blank=True)), + ("note", models.TextField(blank=True)), + ("internal_comments", models.TextField(blank=True)), + ("name", models.CharField(max_length=255)), ], options={ - 'verbose_name': 'document history', - 'verbose_name_plural': 'document histories', + "verbose_name": "document history", + "verbose_name_plural": "document histories", }, ), migrations.CreateModel( - name='DocHistoryAuthor', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('affiliation', models.CharField(blank=True, help_text='Organization/company used by author for submission', max_length=100)), - ('country', models.CharField(blank=True, help_text='Country used by author for submission', max_length=255)), - ('order', models.IntegerField(default=1)), + name="DocHistoryAuthor", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "affiliation", + models.CharField( + blank=True, + help_text="Organization/company used by author for submission", + max_length=100, + ), + ), + ( + "country", + models.CharField( + blank=True, + help_text="Country used by author for submission", + max_length=255, + ), + ), + ("order", models.IntegerField(default=1)), ], options={ - 'ordering': ['document', 'order'], - 'abstract': False, + "ordering": ["document", "order"], + "abstract": False, }, ), migrations.CreateModel( - name='DocReminder', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('due', models.DateTimeField()), - ('active', models.BooleanField(default=True)), - ], - ), - migrations.CreateModel( - name='Document', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('time', models.DateTimeField(default=django.utils.timezone.now)), - ('title', models.CharField(max_length=255, validators=[django.core.validators.RegexValidator(message='Please enter a string without control characters.', regex='^[^\x00-\x1f]*$')])), - ('abstract', models.TextField(blank=True)), - ('rev', models.CharField(blank=True, max_length=16, verbose_name='revision')), - ('pages', models.IntegerField(blank=True, null=True)), - ('words', models.IntegerField(blank=True, null=True)), - ('order', models.IntegerField(blank=True, default=1)), - ('expires', models.DateTimeField(blank=True, null=True)), - ('notify', models.TextField(blank=True, max_length=1023)), - ('external_url', models.URLField(blank=True)), - ('uploaded_filename', models.TextField(blank=True)), - ('note', models.TextField(blank=True)), - ('internal_comments', models.TextField(blank=True)), - ('name', models.CharField(max_length=255, unique=True, validators=[django.core.validators.RegexValidator('^[-a-z0-9]+$', 'Provide a valid document name consisting of lowercase letters, numbers and hyphens.', 'invalid')])), + name="DocReminder", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("due", models.DateTimeField()), + ("active", models.BooleanField(default=True)), + ], + ), + migrations.CreateModel( + name="Document", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("time", models.DateTimeField(default=django.utils.timezone.now)), + ( + "title", + models.CharField( + max_length=255, + validators=[ + django.core.validators.RegexValidator( + message="Please enter a string without control characters.", + regex="^[^\x00-\x1f]*$", + ) + ], + ), + ), + ("abstract", models.TextField(blank=True)), + ( + "rev", + models.CharField( + blank=True, max_length=16, verbose_name="revision" + ), + ), + ("pages", models.IntegerField(blank=True, null=True)), + ("words", models.IntegerField(blank=True, null=True)), + ("order", models.IntegerField(blank=True, default=1)), + ("expires", models.DateTimeField(blank=True, null=True)), + ("notify", models.TextField(blank=True, max_length=1023)), + ("external_url", models.URLField(blank=True)), + ("uploaded_filename", models.TextField(blank=True)), + ("note", models.TextField(blank=True)), + ("internal_comments", models.TextField(blank=True)), + ( + "name", + models.CharField( + max_length=255, + unique=True, + validators=[ + django.core.validators.RegexValidator( + "^[-a-z0-9]+$", + "Provide a valid document name consisting of lowercase letters, numbers and hyphens.", + "invalid", + ) + ], + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.CreateModel( - name='StateType', - fields=[ - ('slug', models.CharField(max_length=30, primary_key=True, serialize=False)), - ('label', models.CharField(help_text='Label that should be used (e.g. in admin) for state drop-down for this type of state', max_length=255)), - ], - ), - migrations.CreateModel( - name='AddedMessageEvent', - fields=[ - ('docevent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='doc.DocEvent')), - ('msgtype', models.CharField(max_length=25)), - ], - bases=('doc.docevent',), - ), - migrations.CreateModel( - name='BallotDocEvent', - fields=[ - ('docevent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='doc.DocEvent')), - ], - bases=('doc.docevent',), - ), - migrations.CreateModel( - name='BallotPositionDocEvent', - fields=[ - ('docevent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='doc.DocEvent')), - ('discuss', models.TextField(blank=True, help_text='Discuss text if position is discuss')), - ('discuss_time', models.DateTimeField(blank=True, help_text='Time discuss text was written', null=True)), - ('comment', models.TextField(blank=True, help_text='Optional comment')), - ('comment_time', models.DateTimeField(blank=True, help_text='Time optional comment was written', null=True)), - ('send_email', models.BooleanField(default=None, null=True)), - ], - bases=('doc.docevent',), - ), - migrations.CreateModel( - name='BofreqEditorDocEvent', - fields=[ - ('docevent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='doc.DocEvent')), - ], - bases=('doc.docevent',), - ), - migrations.CreateModel( - name='BofreqResponsibleDocEvent', - fields=[ - ('docevent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='doc.DocEvent')), - ], - bases=('doc.docevent',), - ), - migrations.CreateModel( - name='ConsensusDocEvent', - fields=[ - ('docevent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='doc.DocEvent')), - ('consensus', models.BooleanField(default=None, null=True)), - ], - bases=('doc.docevent',), - ), - migrations.CreateModel( - name='EditedAuthorsDocEvent', - fields=[ - ('docevent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='doc.DocEvent')), - ('basis', models.CharField(help_text='What is the source or reasoning for the changes to the author list', max_length=255)), - ], - bases=('doc.docevent',), - ), - migrations.CreateModel( - name='IanaExpertDocEvent', - fields=[ - ('docevent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='doc.DocEvent')), - ], - bases=('doc.docevent',), - ), - migrations.CreateModel( - name='InitialReviewDocEvent', - fields=[ - ('docevent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='doc.DocEvent')), - ('expires', models.DateTimeField(blank=True, null=True)), - ], - bases=('doc.docevent',), - ), - migrations.CreateModel( - name='LastCallDocEvent', - fields=[ - ('docevent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='doc.DocEvent')), - ('expires', models.DateTimeField(blank=True, null=True)), - ], - bases=('doc.docevent',), - ), - migrations.CreateModel( - name='NewRevisionDocEvent', - fields=[ - ('docevent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='doc.DocEvent')), - ], - bases=('doc.docevent',), - ), - migrations.CreateModel( - name='ReviewAssignmentDocEvent', - fields=[ - ('docevent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='doc.DocEvent')), - ], - bases=('doc.docevent',), - ), - migrations.CreateModel( - name='ReviewRequestDocEvent', - fields=[ - ('docevent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='doc.DocEvent')), - ], - bases=('doc.docevent',), - ), - migrations.CreateModel( - name='StateDocEvent', - fields=[ - ('docevent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='doc.DocEvent')), - ], - bases=('doc.docevent',), - ), - migrations.CreateModel( - name='SubmissionDocEvent', - fields=[ - ('docevent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='doc.DocEvent')), - ], - bases=('doc.docevent',), - ), - migrations.CreateModel( - name='TelechatDocEvent', - fields=[ - ('docevent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='doc.DocEvent')), - ('telechat_date', models.DateField(blank=True, null=True)), - ('returning_item', models.BooleanField(default=False)), - ], - bases=('doc.docevent',), - ), - migrations.CreateModel( - name='WriteupDocEvent', - fields=[ - ('docevent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='doc.DocEvent')), - ('text', models.TextField(blank=True)), - ], - bases=('doc.docevent',), - ), - migrations.CreateModel( - name='State', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('slug', models.SlugField()), - ('name', models.CharField(max_length=255)), - ('used', models.BooleanField(default=True)), - ('desc', models.TextField(blank=True)), - ('order', models.IntegerField(default=0)), - ('next_states', models.ManyToManyField(blank=True, related_name='previous_states', to='doc.State')), - ('type', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='doc.StateType')), + name="StateType", + fields=[ + ( + "slug", + models.CharField(max_length=30, primary_key=True, serialize=False), + ), + ( + "label", + models.CharField( + help_text="Label that should be used (e.g. in admin) for state drop-down for this type of state", + max_length=255, + ), + ), + ], + ), + migrations.CreateModel( + name="AddedMessageEvent", + fields=[ + ( + "docevent_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="doc.DocEvent", + ), + ), + ("msgtype", models.CharField(max_length=25)), + ], + bases=("doc.docevent",), + ), + migrations.CreateModel( + name="BallotDocEvent", + fields=[ + ( + "docevent_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="doc.DocEvent", + ), + ), + ], + bases=("doc.docevent",), + ), + migrations.CreateModel( + name="BallotPositionDocEvent", + fields=[ + ( + "docevent_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="doc.DocEvent", + ), + ), + ( + "discuss", + models.TextField( + blank=True, help_text="Discuss text if position is discuss" + ), + ), + ( + "discuss_time", + models.DateTimeField( + blank=True, help_text="Time discuss text was written", null=True + ), + ), + ("comment", models.TextField(blank=True, help_text="Optional comment")), + ( + "comment_time", + models.DateTimeField( + blank=True, + help_text="Time optional comment was written", + null=True, + ), + ), + ("send_email", models.BooleanField(default=None, null=True)), + ], + bases=("doc.docevent",), + ), + migrations.CreateModel( + name="BofreqEditorDocEvent", + fields=[ + ( + "docevent_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="doc.DocEvent", + ), + ), + ], + bases=("doc.docevent",), + ), + migrations.CreateModel( + name="BofreqResponsibleDocEvent", + fields=[ + ( + "docevent_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="doc.DocEvent", + ), + ), + ], + bases=("doc.docevent",), + ), + migrations.CreateModel( + name="ConsensusDocEvent", + fields=[ + ( + "docevent_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="doc.DocEvent", + ), + ), + ("consensus", models.BooleanField(default=None, null=True)), + ], + bases=("doc.docevent",), + ), + migrations.CreateModel( + name="EditedAuthorsDocEvent", + fields=[ + ( + "docevent_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="doc.DocEvent", + ), + ), + ( + "basis", + models.CharField( + help_text="What is the source or reasoning for the changes to the author list", + max_length=255, + ), + ), + ], + bases=("doc.docevent",), + ), + migrations.CreateModel( + name="IanaExpertDocEvent", + fields=[ + ( + "docevent_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="doc.DocEvent", + ), + ), + ], + bases=("doc.docevent",), + ), + migrations.CreateModel( + name="InitialReviewDocEvent", + fields=[ + ( + "docevent_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="doc.DocEvent", + ), + ), + ("expires", models.DateTimeField(blank=True, null=True)), + ], + bases=("doc.docevent",), + ), + migrations.CreateModel( + name="LastCallDocEvent", + fields=[ + ( + "docevent_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="doc.DocEvent", + ), + ), + ("expires", models.DateTimeField(blank=True, null=True)), + ], + bases=("doc.docevent",), + ), + migrations.CreateModel( + name="NewRevisionDocEvent", + fields=[ + ( + "docevent_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="doc.DocEvent", + ), + ), + ], + bases=("doc.docevent",), + ), + migrations.CreateModel( + name="ReviewAssignmentDocEvent", + fields=[ + ( + "docevent_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="doc.DocEvent", + ), + ), + ], + bases=("doc.docevent",), + ), + migrations.CreateModel( + name="ReviewRequestDocEvent", + fields=[ + ( + "docevent_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="doc.DocEvent", + ), + ), + ], + bases=("doc.docevent",), + ), + migrations.CreateModel( + name="StateDocEvent", + fields=[ + ( + "docevent_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="doc.DocEvent", + ), + ), + ], + bases=("doc.docevent",), + ), + migrations.CreateModel( + name="SubmissionDocEvent", + fields=[ + ( + "docevent_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="doc.DocEvent", + ), + ), + ], + bases=("doc.docevent",), + ), + migrations.CreateModel( + name="TelechatDocEvent", + fields=[ + ( + "docevent_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="doc.DocEvent", + ), + ), + ("telechat_date", models.DateField(blank=True, null=True)), + ("returning_item", models.BooleanField(default=False)), + ], + bases=("doc.docevent",), + ), + migrations.CreateModel( + name="WriteupDocEvent", + fields=[ + ( + "docevent_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="doc.DocEvent", + ), + ), + ("text", models.TextField(blank=True)), + ], + bases=("doc.docevent",), + ), + migrations.CreateModel( + name="State", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("slug", models.SlugField()), + ("name", models.CharField(max_length=255)), + ("used", models.BooleanField(default=True)), + ("desc", models.TextField(blank=True)), + ("order", models.IntegerField(default=0)), + ( + "next_states", + models.ManyToManyField( + blank=True, related_name="previous_states", to="doc.State" + ), + ), + ( + "type", + ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="doc.StateType" + ), + ), ], options={ - 'ordering': ['type', 'order'], + "ordering": ["type", "order"], }, ), migrations.CreateModel( - name='RelatedDocument', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('relationship', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='name.DocRelationshipName')), - ('source', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='doc.Document')), - ('target', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='doc.DocAlias')), - ], - ), - migrations.CreateModel( - name='RelatedDocHistory', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('relationship', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='name.DocRelationshipName')), - ('source', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='doc.DocHistory')), - ('target', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reversely_related_document_history_set', to='doc.DocAlias')), - ], - ), - migrations.CreateModel( - name='DocumentURL', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('desc', models.CharField(blank=True, default='', max_length=255)), - ('url', models.URLField(max_length=2083)), - ('doc', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='doc.Document')), - ('tag', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='name.DocUrlTagName')), - ], - ), - migrations.CreateModel( - name='DocumentAuthor', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('affiliation', models.CharField(blank=True, help_text='Organization/company used by author for submission', max_length=100)), - ('country', models.CharField(blank=True, help_text='Country used by author for submission', max_length=255)), - ('order', models.IntegerField(default=1)), - ('document', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='doc.Document')), - ('email', ietf.utils.models.ForeignKey(blank=True, help_text='Email address used by author for submission', null=True, on_delete=django.db.models.deletion.CASCADE, to='person.Email')), - ('person', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='person.Person')), + name="RelatedDocument", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "relationship", + ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="name.DocRelationshipName", + ), + ), + ( + "source", + ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="doc.Document" + ), + ), + ( + "target", + ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="doc.DocAlias" + ), + ), + ], + ), + migrations.CreateModel( + name="RelatedDocHistory", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "relationship", + ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="name.DocRelationshipName", + ), + ), + ( + "source", + ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="doc.DocHistory" + ), + ), + ( + "target", + ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="reversely_related_document_history_set", + to="doc.DocAlias", + ), + ), + ], + ), + migrations.CreateModel( + name="DocumentURL", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("desc", models.CharField(blank=True, default="", max_length=255)), + ("url", models.URLField(max_length=2083)), + ( + "doc", + ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="doc.Document" + ), + ), + ( + "tag", + ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="name.DocUrlTagName", + ), + ), + ], + ), + migrations.CreateModel( + name="DocumentAuthor", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "affiliation", + models.CharField( + blank=True, + help_text="Organization/company used by author for submission", + max_length=100, + ), + ), + ( + "country", + models.CharField( + blank=True, + help_text="Country used by author for submission", + max_length=255, + ), + ), + ("order", models.IntegerField(default=1)), + ( + "document", + ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="doc.Document" + ), + ), + ( + "email", + ietf.utils.models.ForeignKey( + blank=True, + help_text="Email address used by author for submission", + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="person.Email", + ), + ), + ( + "person", + ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="person.Person" + ), + ), ], options={ - 'ordering': ['document', 'order'], - 'abstract': False, + "ordering": ["document", "order"], + "abstract": False, }, ), migrations.CreateModel( - name='DocumentActionHolder', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('time_added', models.DateTimeField(default=django.utils.timezone.now)), - ('document', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='doc.Document')), - ('person', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='person.Person')), + name="DocumentActionHolder", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("time_added", models.DateTimeField(default=django.utils.timezone.now)), + ( + "document", + ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="doc.Document" + ), + ), + ( + "person", + ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="person.Person" + ), + ), ], ), migrations.AddField( - model_name='document', - name='action_holders', - field=models.ManyToManyField(blank=True, through='doc.DocumentActionHolder', to='person.Person'), + model_name="document", + name="action_holders", + field=models.ManyToManyField( + blank=True, through="doc.DocumentActionHolder", to="person.Person" + ), ), migrations.AddField( - model_name='document', - name='ad', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='ad_document_set', to='person.Person', verbose_name='area director'), + model_name="document", + name="ad", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="ad_document_set", + to="person.Person", + verbose_name="area director", + ), ), migrations.AddField( - model_name='document', - name='formal_languages', - field=models.ManyToManyField(blank=True, help_text='Formal languages used in document', to='name.FormalLanguageName'), + model_name="document", + name="formal_languages", + field=models.ManyToManyField( + blank=True, + help_text="Formal languages used in document", + to="name.FormalLanguageName", + ), ), ] diff --git a/ietf/doc/migrations/0002_auto_20230320_1222.py b/ietf/doc/migrations/0002_auto_20230320_1222.py index 90b2d11a25..b94bf993a4 100644 --- a/ietf/doc/migrations/0002_auto_20230320_1222.py +++ b/ietf/doc/migrations/0002_auto_20230320_1222.py @@ -10,283 +10,469 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('message', '0001_initial'), - ('name', '0001_initial'), - ('person', '0001_initial'), - ('review', '0001_initial'), - ('group', '0001_initial'), - ('contenttypes', '0002_remove_content_type_name'), - ('submit', '0001_initial'), - ('doc', '0001_initial'), + ("message", "0001_initial"), + ("name", "0001_initial"), + ("person", "0001_initial"), + ("review", "0001_initial"), + ("group", "0001_initial"), + ("contenttypes", "0002_remove_content_type_name"), + ("submit", "0001_initial"), + ("doc", "0001_initial"), ] operations = [ migrations.AddField( - model_name='document', - name='group', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='group.Group'), + model_name="document", + name="group", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="group.Group", + ), + ), + migrations.AddField( + model_name="document", + name="intended_std_level", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="name.IntendedStdLevelName", + verbose_name="Intended standardization level", + ), + ), + migrations.AddField( + model_name="document", + name="shepherd", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="shepherd_document_set", + to="person.Email", + ), + ), + migrations.AddField( + model_name="document", + name="states", + field=models.ManyToManyField(blank=True, to="doc.State"), + ), + migrations.AddField( + model_name="document", + name="std_level", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="name.StdLevelName", + verbose_name="Standardization level", + ), + ), + migrations.AddField( + model_name="document", + name="stream", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="name.StreamName", + ), + ), + migrations.AddField( + model_name="document", + name="tags", + field=models.ManyToManyField(blank=True, to="name.DocTagName"), + ), + migrations.AddField( + model_name="document", + name="type", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="name.DocTypeName", + ), + ), + migrations.AddField( + model_name="docreminder", + name="event", + field=ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="doc.DocEvent" + ), + ), + migrations.AddField( + model_name="docreminder", + name="type", + field=ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="name.DocReminderTypeName", + ), + ), + migrations.AddField( + model_name="dochistoryauthor", + name="document", + field=ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="documentauthor_set", + to="doc.DocHistory", + ), + ), + migrations.AddField( + model_name="dochistoryauthor", + name="email", + field=ietf.utils.models.ForeignKey( + blank=True, + help_text="Email address used by author for submission", + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="person.Email", + ), + ), + migrations.AddField( + model_name="dochistoryauthor", + name="person", + field=ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="person.Person" + ), + ), + migrations.AddField( + model_name="dochistory", + name="ad", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="ad_dochistory_set", + to="person.Person", + verbose_name="area director", + ), + ), + migrations.AddField( + model_name="dochistory", + name="doc", + field=ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="history_set", + to="doc.Document", + ), + ), + migrations.AddField( + model_name="dochistory", + name="formal_languages", + field=models.ManyToManyField( + blank=True, + help_text="Formal languages used in document", + to="name.FormalLanguageName", + ), + ), + migrations.AddField( + model_name="dochistory", + name="group", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="group.Group", + ), + ), + migrations.AddField( + model_name="dochistory", + name="intended_std_level", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="name.IntendedStdLevelName", + verbose_name="Intended standardization level", + ), + ), + migrations.AddField( + model_name="dochistory", + name="shepherd", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="shepherd_dochistory_set", + to="person.Email", + ), + ), + migrations.AddField( + model_name="dochistory", + name="states", + field=models.ManyToManyField(blank=True, to="doc.State"), + ), + migrations.AddField( + model_name="dochistory", + name="std_level", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="name.StdLevelName", + verbose_name="Standardization level", + ), + ), + migrations.AddField( + model_name="dochistory", + name="stream", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="name.StreamName", + ), + ), + migrations.AddField( + model_name="dochistory", + name="tags", + field=models.ManyToManyField(blank=True, to="name.DocTagName"), ), - migrations.AddField( - model_name='document', - name='intended_std_level', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='name.IntendedStdLevelName', verbose_name='Intended standardization level'), - ), - migrations.AddField( - model_name='document', - name='shepherd', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='shepherd_document_set', to='person.Email'), - ), - migrations.AddField( - model_name='document', - name='states', - field=models.ManyToManyField(blank=True, to='doc.State'), - ), - migrations.AddField( - model_name='document', - name='std_level', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='name.StdLevelName', verbose_name='Standardization level'), - ), - migrations.AddField( - model_name='document', - name='stream', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='name.StreamName'), - ), - migrations.AddField( - model_name='document', - name='tags', - field=models.ManyToManyField(blank=True, to='name.DocTagName'), - ), - migrations.AddField( - model_name='document', - name='type', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='name.DocTypeName'), - ), - migrations.AddField( - model_name='docreminder', - name='event', - field=ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='doc.DocEvent'), - ), - migrations.AddField( - model_name='docreminder', - name='type', - field=ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='name.DocReminderTypeName'), - ), - migrations.AddField( - model_name='dochistoryauthor', - name='document', - field=ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documentauthor_set', to='doc.DocHistory'), - ), - migrations.AddField( - model_name='dochistoryauthor', - name='email', - field=ietf.utils.models.ForeignKey(blank=True, help_text='Email address used by author for submission', null=True, on_delete=django.db.models.deletion.CASCADE, to='person.Email'), - ), - migrations.AddField( - model_name='dochistoryauthor', - name='person', - field=ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='person.Person'), - ), - migrations.AddField( - model_name='dochistory', - name='ad', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='ad_dochistory_set', to='person.Person', verbose_name='area director'), - ), - migrations.AddField( - model_name='dochistory', - name='doc', - field=ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='history_set', to='doc.Document'), - ), - migrations.AddField( - model_name='dochistory', - name='formal_languages', - field=models.ManyToManyField(blank=True, help_text='Formal languages used in document', to='name.FormalLanguageName'), - ), - migrations.AddField( - model_name='dochistory', - name='group', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='group.Group'), + migrations.AddField( + model_name="dochistory", + name="type", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="name.DocTypeName", + ), + ), + migrations.AddField( + model_name="docextresource", + name="doc", + field=ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="doc.Document" + ), ), migrations.AddField( - model_name='dochistory', - name='intended_std_level', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='name.IntendedStdLevelName', verbose_name='Intended standardization level'), + model_name="docextresource", + name="name", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="name.ExtResourceName" + ), ), migrations.AddField( - model_name='dochistory', - name='shepherd', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='shepherd_dochistory_set', to='person.Email'), + model_name="docevent", + name="by", + field=ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="person.Person" + ), ), migrations.AddField( - model_name='dochistory', - name='states', - field=models.ManyToManyField(blank=True, to='doc.State'), + model_name="docevent", + name="doc", + field=ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="doc.Document" + ), ), migrations.AddField( - model_name='dochistory', - name='std_level', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='name.StdLevelName', verbose_name='Standardization level'), + model_name="docalias", + name="docs", + field=models.ManyToManyField(related_name="docalias", to="doc.Document"), ), migrations.AddField( - model_name='dochistory', - name='stream', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='name.StreamName'), + model_name="deletedevent", + name="by", + field=ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="person.Person" + ), ), migrations.AddField( - model_name='dochistory', - name='tags', - field=models.ManyToManyField(blank=True, to='name.DocTagName'), + model_name="deletedevent", + name="content_type", + field=ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="contenttypes.ContentType", + ), ), migrations.AddField( - model_name='dochistory', - name='type', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='name.DocTypeName'), + model_name="ballottype", + name="doc_type", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="name.DocTypeName", + ), ), migrations.AddField( - model_name='docextresource', - name='doc', - field=ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='doc.Document'), - ), - migrations.AddField( - model_name='docextresource', - name='name', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='name.ExtResourceName'), - ), - migrations.AddField( - model_name='docevent', - name='by', - field=ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='person.Person'), - ), - migrations.AddField( - model_name='docevent', - name='doc', - field=ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='doc.Document'), - ), - migrations.AddField( - model_name='docalias', - name='docs', - field=models.ManyToManyField(related_name='docalias', to='doc.Document'), - ), - migrations.AddField( - model_name='deletedevent', - name='by', - field=ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='person.Person'), - ), - migrations.AddField( - model_name='deletedevent', - name='content_type', - field=ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'), - ), - migrations.AddField( - model_name='ballottype', - name='doc_type', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='name.DocTypeName'), - ), - migrations.AddField( - model_name='ballottype', - name='positions', - field=models.ManyToManyField(blank=True, to='name.BallotPositionName'), + model_name="ballottype", + name="positions", + field=models.ManyToManyField(blank=True, to="name.BallotPositionName"), ), migrations.CreateModel( - name='IRSGBallotDocEvent', + name="IRSGBallotDocEvent", fields=[ - ('ballotdocevent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='doc.BallotDocEvent')), - ('duedate', models.DateTimeField(blank=True, null=True)), + ( + "ballotdocevent_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="doc.BallotDocEvent", + ), + ), + ("duedate", models.DateTimeField(blank=True, null=True)), ], - bases=('doc.ballotdocevent',), + bases=("doc.ballotdocevent",), ), migrations.AddField( - model_name='submissiondocevent', - name='submission', - field=ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='submit.Submission'), + model_name="submissiondocevent", + name="submission", + field=ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="submit.Submission" + ), ), migrations.AddField( - model_name='statedocevent', - name='state', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='doc.State'), + model_name="statedocevent", + name="state", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="doc.State", + ), ), migrations.AddField( - model_name='statedocevent', - name='state_type', - field=ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='doc.StateType'), + model_name="statedocevent", + name="state_type", + field=ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="doc.StateType" + ), ), migrations.AddField( - model_name='reviewrequestdocevent', - name='review_request', - field=ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='review.ReviewRequest'), + model_name="reviewrequestdocevent", + name="review_request", + field=ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="review.ReviewRequest" + ), ), migrations.AddField( - model_name='reviewrequestdocevent', - name='state', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='name.ReviewRequestStateName'), + model_name="reviewrequestdocevent", + name="state", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="name.ReviewRequestStateName", + ), ), migrations.AddField( - model_name='reviewassignmentdocevent', - name='review_assignment', - field=ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='review.ReviewAssignment'), + model_name="reviewassignmentdocevent", + name="review_assignment", + field=ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="review.ReviewAssignment", + ), ), migrations.AddField( - model_name='reviewassignmentdocevent', - name='state', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='name.ReviewAssignmentStateName'), + model_name="reviewassignmentdocevent", + name="state", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="name.ReviewAssignmentStateName", + ), ), migrations.AddIndex( - model_name='documentauthor', - index=models.Index(fields=['document', 'order'], name='doc_documen_documen_7fabe2_idx'), + model_name="documentauthor", + index=models.Index( + fields=["document", "order"], name="doc_documen_documen_7fabe2_idx" + ), ), migrations.AddConstraint( - model_name='documentactionholder', - constraint=models.UniqueConstraint(fields=('document', 'person'), name='unique_action_holder'), + model_name="documentactionholder", + constraint=models.UniqueConstraint( + fields=("document", "person"), name="unique_action_holder" + ), ), migrations.AddIndex( - model_name='dochistoryauthor', - index=models.Index(fields=['document', 'order'], name='doc_dochist_documen_7e2441_idx'), + model_name="dochistoryauthor", + index=models.Index( + fields=["document", "order"], name="doc_dochist_documen_7e2441_idx" + ), ), migrations.AddIndex( - model_name='docevent', - index=models.Index(fields=['type', 'doc'], name='doc_doceven_type_43e53e_idx'), + model_name="docevent", + index=models.Index( + fields=["type", "doc"], name="doc_doceven_type_43e53e_idx" + ), ), migrations.AddIndex( - model_name='docevent', - index=models.Index(fields=['-time', '-id'], name='doc_doceven_time_1a258f_idx'), - ), - migrations.AddField( - model_name='bofreqresponsibledocevent', - name='responsible', - field=models.ManyToManyField(blank=True, to='person.Person'), - ), - migrations.AddField( - model_name='bofreqeditordocevent', - name='editors', - field=models.ManyToManyField(blank=True, to='person.Person'), - ), - migrations.AddField( - model_name='ballotpositiondocevent', - name='ballot', - field=ietf.utils.models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='doc.BallotDocEvent'), - ), - migrations.AddField( - model_name='ballotpositiondocevent', - name='balloter', - field=ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='person.Person'), - ), - migrations.AddField( - model_name='ballotpositiondocevent', - name='pos', - field=ietf.utils.models.ForeignKey(default='norecord', on_delete=django.db.models.deletion.CASCADE, to='name.BallotPositionName', verbose_name='position'), - ), - migrations.AddField( - model_name='ballotdocevent', - name='ballot_type', - field=ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='doc.BallotType'), - ), - migrations.AddField( - model_name='addedmessageevent', - name='in_reply_to', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='doc_irtomanual', to='message.Message'), - ), - migrations.AddField( - model_name='addedmessageevent', - name='message', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='doc_manualevents', to='message.Message'), + model_name="docevent", + index=models.Index( + fields=["-time", "-id"], name="doc_doceven_time_1a258f_idx" + ), + ), + migrations.AddField( + model_name="bofreqresponsibledocevent", + name="responsible", + field=models.ManyToManyField(blank=True, to="person.Person"), + ), + migrations.AddField( + model_name="bofreqeditordocevent", + name="editors", + field=models.ManyToManyField(blank=True, to="person.Person"), + ), + migrations.AddField( + model_name="ballotpositiondocevent", + name="ballot", + field=ietf.utils.models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="doc.BallotDocEvent", + ), + ), + migrations.AddField( + model_name="ballotpositiondocevent", + name="balloter", + field=ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="person.Person" + ), + ), + migrations.AddField( + model_name="ballotpositiondocevent", + name="pos", + field=ietf.utils.models.ForeignKey( + default="norecord", + on_delete=django.db.models.deletion.CASCADE, + to="name.BallotPositionName", + verbose_name="position", + ), + ), + migrations.AddField( + model_name="ballotdocevent", + name="ballot_type", + field=ietf.utils.models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="doc.BallotType" + ), + ), + migrations.AddField( + model_name="addedmessageevent", + name="in_reply_to", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="doc_irtomanual", + to="message.Message", + ), + ), + migrations.AddField( + model_name="addedmessageevent", + name="message", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="doc_manualevents", + to="message.Message", + ), ), ] diff --git a/ietf/doc/migrations/0003_remove_document_info_order.py b/ietf/doc/migrations/0003_remove_document_info_order.py index dcd324b71f..698f563a3c 100644 --- a/ietf/doc/migrations/0003_remove_document_info_order.py +++ b/ietf/doc/migrations/0003_remove_document_info_order.py @@ -5,16 +5,16 @@ class Migration(migrations.Migration): dependencies = [ - ('doc', '0002_auto_20230320_1222'), + ("doc", "0002_auto_20230320_1222"), ] operations = [ migrations.RemoveField( - model_name='dochistory', - name='order', + model_name="dochistory", + name="order", ), migrations.RemoveField( - model_name='document', - name='order', + model_name="document", + name="order", ), ] diff --git a/ietf/doc/migrations/0004_alter_dochistory_ad_alter_dochistory_shepherd_and_more.py b/ietf/doc/migrations/0004_alter_dochistory_ad_alter_dochistory_shepherd_and_more.py index adc0e69627..c480e51921 100644 --- a/ietf/doc/migrations/0004_alter_dochistory_ad_alter_dochistory_shepherd_and_more.py +++ b/ietf/doc/migrations/0004_alter_dochistory_ad_alter_dochistory_shepherd_and_more.py @@ -8,29 +8,55 @@ class Migration(migrations.Migration): dependencies = [ - ('person', '0001_initial'), - ('doc', '0003_remove_document_info_order'), + ("person", "0001_initial"), + ("doc", "0003_remove_document_info_order"), ] operations = [ migrations.AlterField( - model_name='dochistory', - name='ad', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='ad_%(class)s_set', to='person.person', verbose_name='area director'), + model_name="dochistory", + name="ad", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="ad_%(class)s_set", + to="person.person", + verbose_name="area director", + ), ), migrations.AlterField( - model_name='dochistory', - name='shepherd', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='shepherd_%(class)s_set', to='person.email'), + model_name="dochistory", + name="shepherd", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="shepherd_%(class)s_set", + to="person.email", + ), ), migrations.AlterField( - model_name='document', - name='ad', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='ad_%(class)s_set', to='person.person', verbose_name='area director'), + model_name="document", + name="ad", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="ad_%(class)s_set", + to="person.person", + verbose_name="area director", + ), ), migrations.AlterField( - model_name='document', - name='shepherd', - field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='shepherd_%(class)s_set', to='person.email'), + model_name="document", + name="shepherd", + field=ietf.utils.models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="shepherd_%(class)s_set", + to="person.email", + ), ), ] diff --git a/ietf/doc/migrations/0011_create_rfc_documents.py b/ietf/doc/migrations/0011_create_rfc_documents.py index 466ff81bb0..b1fa1e206f 100644 --- a/ietf/doc/migrations/0011_create_rfc_documents.py +++ b/ietf/doc/migrations/0011_create_rfc_documents.py @@ -7,24 +7,28 @@ def forward(apps, schema_editor): Document = apps.get_model("doc", "Document") DocAlias = apps.get_model("doc", "DocAlias") DocumentAuthor = apps.get_model("doc", "DocumentAuthor") - + State = apps.get_model("doc", "State") draft_rfc_state = State.objects.get(type_id="draft", slug="rfc") rfc_published_state = State.objects.get(type_id="rfc", slug="published") - + # Find draft Documents in the "rfc" state found_by_state = Document.objects.filter(states=draft_rfc_state).distinct() - + # Find Documents with an "rfc..." alias and confirm they're the same set rfc_docaliases = DocAlias.objects.filter(name__startswith="rfc") found_by_name = Document.objects.filter(docalias__in=rfc_docaliases).distinct() - assert set(found_by_name) == set(found_by_state), "mismatch between rfcs identified by state and docalias" - - # As of 2023-06-15, there is one Document with two rfc aliases: rfc6312 and rfc6342 are the same Document. This + assert set(found_by_name) == set( + found_by_state + ), "mismatch between rfcs identified by state and docalias" + + # As of 2023-06-15, there is one Document with two rfc aliases: rfc6312 and rfc6342 are the same Document. This # was due to a publication error. Because we go alias-by-alias, no special handling is needed in this migration. - + for rfc_alias in rfc_docaliases.order_by("name"): - assert rfc_alias.docs.count() == 1, f"DocAlias {rfc_alias} is linked to more than 1 Document" + assert ( + rfc_alias.docs.count() == 1 + ), f"DocAlias {rfc_alias} is linked to more than 1 Document" draft = rfc_alias.docs.first() if draft.name.startswith("rfc"): rfc = draft @@ -52,7 +56,7 @@ def forward(apps, schema_editor): ) rfc.states.set([rfc_published_state]) rfc.formal_languages.set(draft.formal_languages.all()) - + # Copy Authors for da in draft.documentauthor_set.all(): DocumentAuthor.objects.create( diff --git a/ietf/doc/migrations/0014_move_rfc_docaliases.py b/ietf/doc/migrations/0014_move_rfc_docaliases.py index c82a98e052..3ab500495f 100644 --- a/ietf/doc/migrations/0014_move_rfc_docaliases.py +++ b/ietf/doc/migrations/0014_move_rfc_docaliases.py @@ -14,17 +14,21 @@ def forward(apps, schema_editor): for rfc_alias in DocAlias.objects.filter(name__startswith="rfc"): rfc = Document.objects.get(name=rfc_alias.name) - aliased_doc = rfc_alias.docs.get() # implicitly confirms only one value in rfc_alias.docs + aliased_doc = ( + rfc_alias.docs.get() + ) # implicitly confirms only one value in rfc_alias.docs if aliased_doc != rfc: # If the DocAlias was not already pointing at the rfc, it was pointing at the draft # it came from. Create the relationship between draft and rfc Documents. - assert aliased_doc.type_id == "draft", f"Alias for {rfc.name} should be pointing at a draft" + assert ( + aliased_doc.type_id == "draft" + ), f"Alias for {rfc.name} should be pointing at a draft" RelatedDocument.objects.create( source=aliased_doc, target=rfc_alias, relationship_id="became_rfc", ) - # Now move the alias from the draft to the rfc + # Now move the alias from the draft to the rfc rfc_alias.docs.set([rfc]) diff --git a/ietf/doc/migrations/0015_relate_no_aliases.py b/ietf/doc/migrations/0015_relate_no_aliases.py index 4ba3dd9607..12ae1fe384 100644 --- a/ietf/doc/migrations/0015_relate_no_aliases.py +++ b/ietf/doc/migrations/0015_relate_no_aliases.py @@ -5,16 +5,25 @@ from django.db.models import F, Subquery, OuterRef, CharField import ietf.utils.models + def forward(apps, schema_editor): RelatedDocument = apps.get_model("doc", "RelatedDocument") DocAlias = apps.get_model("doc", "DocAlias") - target_subquery = Subquery(DocAlias.objects.filter(pk=OuterRef("deprecated_target")).values("docs")[:1]) - name_subquery = Subquery(DocAlias.objects.filter(pk=OuterRef("deprecated_target")).values("name")[:1]) - RelatedDocument.objects.annotate(firstdoc=target_subquery).annotate(aliasname=name_subquery).update(target=F("firstdoc"),originaltargetaliasname=F("aliasname")) + target_subquery = Subquery( + DocAlias.objects.filter(pk=OuterRef("deprecated_target")).values("docs")[:1] + ) + name_subquery = Subquery( + DocAlias.objects.filter(pk=OuterRef("deprecated_target")).values("name")[:1] + ) + RelatedDocument.objects.annotate(firstdoc=target_subquery).annotate( + aliasname=name_subquery + ).update(target=F("firstdoc"), originaltargetaliasname=F("aliasname")) + def reverse(apps, schema_editor): pass + class Migration(migrations.Migration): dependencies = [ ("doc", "0014_move_rfc_docaliases"), @@ -22,33 +31,33 @@ class Migration(migrations.Migration): operations = [ migrations.AlterField( - model_name='relateddocument', - name='target', + model_name="relateddocument", + name="target", field=ietf.utils.models.ForeignKey( db_index=False, on_delete=django.db.models.deletion.CASCADE, - to='doc.docalias', + to="doc.docalias", ), ), migrations.RenameField( model_name="relateddocument", old_name="target", - new_name="deprecated_target" + new_name="deprecated_target", ), migrations.AlterField( - model_name='relateddocument', - name='deprecated_target', + model_name="relateddocument", + name="deprecated_target", field=ietf.utils.models.ForeignKey( db_index=True, on_delete=django.db.models.deletion.CASCADE, - to='doc.docalias', + to="doc.docalias", ), ), migrations.AddField( model_name="relateddocument", name="target", field=ietf.utils.models.ForeignKey( - default=1, # A lie, but a convenient one - no relations point here. + default=1, # A lie, but a convenient one - no relations point here. on_delete=django.db.models.deletion.CASCADE, related_name="targets_related", to="doc.document", @@ -59,7 +68,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name="relateddocument", name="originaltargetaliasname", - field=CharField(max_length=255,null=True,blank=True), + field=CharField(max_length=255, null=True, blank=True), preserve_default=True, ), migrations.RunPython(forward, reverse), @@ -78,7 +87,7 @@ class Migration(migrations.Migration): name="deprecated_target", field=ietf.utils.models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, - to='doc.DocAlias', + to="doc.DocAlias", ), ), ] diff --git a/ietf/doc/migrations/0016_relate_hist_no_aliases.py b/ietf/doc/migrations/0016_relate_hist_no_aliases.py index df5fb3c325..ebdd4a3dfe 100644 --- a/ietf/doc/migrations/0016_relate_hist_no_aliases.py +++ b/ietf/doc/migrations/0016_relate_hist_no_aliases.py @@ -5,16 +5,25 @@ from django.db.models import F, Subquery, OuterRef, CharField import ietf.utils.models + def forward(apps, schema_editor): RelatedDocHistory = apps.get_model("doc", "RelatedDocHistory") DocAlias = apps.get_model("doc", "DocAlias") - target_subquery = Subquery(DocAlias.objects.filter(pk=OuterRef("deprecated_target")).values("docs")[:1]) - name_subquery = Subquery(DocAlias.objects.filter(pk=OuterRef("deprecated_target")).values("name")[:1]) - RelatedDocHistory.objects.annotate(firstdoc=target_subquery).annotate(aliasname=name_subquery).update(target=F("firstdoc"),originaltargetaliasname=F("aliasname")) + target_subquery = Subquery( + DocAlias.objects.filter(pk=OuterRef("deprecated_target")).values("docs")[:1] + ) + name_subquery = Subquery( + DocAlias.objects.filter(pk=OuterRef("deprecated_target")).values("name")[:1] + ) + RelatedDocHistory.objects.annotate(firstdoc=target_subquery).annotate( + aliasname=name_subquery + ).update(target=F("firstdoc"), originaltargetaliasname=F("aliasname")) + def reverse(apps, schema_editor): pass + class Migration(migrations.Migration): dependencies = [ ("doc", "0015_relate_no_aliases"), @@ -22,46 +31,46 @@ class Migration(migrations.Migration): operations = [ migrations.AlterField( - model_name='relateddochistory', - name='target', + model_name="relateddochistory", + name="target", field=ietf.utils.models.ForeignKey( db_index=False, on_delete=django.db.models.deletion.CASCADE, - to='doc.docalias', - related_name='reversely_related_document_history_set', + to="doc.docalias", + related_name="reversely_related_document_history_set", ), ), migrations.RenameField( model_name="relateddochistory", old_name="target", - new_name="deprecated_target" + new_name="deprecated_target", ), migrations.AlterField( - model_name='relateddochistory', - name='deprecated_target', + model_name="relateddochistory", + name="deprecated_target", field=ietf.utils.models.ForeignKey( db_index=True, on_delete=django.db.models.deletion.CASCADE, - to='doc.docalias', - related_name='deprecated_reversely_related_document_history_set', + to="doc.docalias", + related_name="deprecated_reversely_related_document_history_set", ), ), migrations.AddField( model_name="relateddochistory", name="target", field=ietf.utils.models.ForeignKey( - default=1, # A lie, but a convenient one - no relations point here. + default=1, # A lie, but a convenient one - no relations point here. on_delete=django.db.models.deletion.CASCADE, to="doc.document", db_index=False, - related_name='reversely_related_document_history_set', + related_name="reversely_related_document_history_set", ), preserve_default=False, ), migrations.AddField( model_name="relateddochistory", name="originaltargetaliasname", - field=CharField(max_length=255,null=True,blank=True), + field=CharField(max_length=255, null=True, blank=True), preserve_default=True, ), migrations.RunPython(forward, reverse), @@ -72,7 +81,7 @@ class Migration(migrations.Migration): on_delete=django.db.models.deletion.CASCADE, to="doc.document", db_index=True, - related_name='reversely_related_document_history_set', + related_name="reversely_related_document_history_set", ), ), migrations.RemoveField( @@ -80,8 +89,8 @@ class Migration(migrations.Migration): name="deprecated_target", field=ietf.utils.models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, - to='doc.DocAlias', - related_name='deprecated_reversely_related_document_history_set', + to="doc.DocAlias", + related_name="deprecated_reversely_related_document_history_set", ), ), ] diff --git a/ietf/doc/migrations/0024_remove_ad_is_watching_states.py b/ietf/doc/migrations/0024_remove_ad_is_watching_states.py index 0c0fb0ad25..99146e9bac 100644 --- a/ietf/doc/migrations/0024_remove_ad_is_watching_states.py +++ b/ietf/doc/migrations/0024_remove_ad_is_watching_states.py @@ -3,36 +3,43 @@ from django.db import migrations -def get_helper(DocHistory, RelatedDocument, RelatedDocHistory, DocumentAuthor, DocHistoryAuthor): +def get_helper( + DocHistory, RelatedDocument, RelatedDocHistory, DocumentAuthor, DocHistoryAuthor +): """Dependency injection wrapper""" def save_document_in_history(doc): """Save a snapshot of document and related objects in the database. - + Local copy of ietf.doc.utils.save_document_in_history() to avoid depending on the code base in a migration. """ - + def get_model_fields_as_dict(obj): - return dict((field.name, getattr(obj, field.name)) - for field in obj._meta.fields - if field is not obj._meta.pk) - + return dict( + (field.name, getattr(obj, field.name)) + for field in obj._meta.fields + if field is not obj._meta.pk + ) + # copy fields fields = get_model_fields_as_dict(doc) fields["doc"] = doc fields["name"] = doc.name - + dochist = DocHistory(**fields) dochist.save() - + # copy many to many for field in doc._meta.many_to_many: - if field.remote_field.through and field.remote_field.through._meta.auto_created: + if ( + field.remote_field.through + and field.remote_field.through._meta.auto_created + ): hist_field = getattr(dochist, field.name) hist_field.clear() hist_field.set(getattr(doc, field.name).all()) - + # copy remaining tricky many to many def transfer_fields(obj, HistModel): mfields = get_model_fields_as_dict(item) @@ -41,15 +48,15 @@ def transfer_fields(obj, HistModel): if v == doc: mfields[k] = dochist HistModel.objects.create(**mfields) - + for item in RelatedDocument.objects.filter(source=doc): transfer_fields(item, RelatedDocHistory) - + for item in DocumentAuthor.objects.filter(document=doc): transfer_fields(item, DocHistoryAuthor) - + return dochist - + return save_document_in_history @@ -60,7 +67,7 @@ def forward(apps, schema_editor): State = apps.get_model("doc", "State") StateType = apps.get_model("doc", "StateType") Person = apps.get_model("person", "Person") - + save_document_in_history = get_helper( DocHistory=apps.get_model("doc", "DocHistory"), RelatedDocument=apps.get_model("doc", "RelatedDocument"), @@ -100,7 +107,7 @@ def forward(apps, schema_editor): def reverse(apps, schema_editor): """Mark watching draft-iesg state as used - + Does not try to re-apply the state to Documents modified by the forward migration. This could be done in theory, but would either require dangerous history rewriting or add a lot of history junk. diff --git a/ietf/doc/models.py b/ietf/doc/models.py index 55da70972c..0b660099cb 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -26,20 +26,32 @@ from django.conf import settings from django.utils import timezone from django.utils.encoding import force_str -from django.utils.html import mark_safe # type:ignore +from django.utils.html import mark_safe # type:ignore from django.contrib.staticfiles import finders -import debug # pyflakes:ignore +import debug # pyflakes:ignore from ietf.group.models import Group from ietf.doc.storage_utils import ( store_str as utils_store_str, store_bytes as utils_store_bytes, - store_file as utils_store_file + store_file as utils_store_file, +) +from ietf.name.models import ( + DocTypeName, + DocTagName, + StreamName, + IntendedStdLevelName, + StdLevelName, + DocRelationshipName, + DocReminderTypeName, + BallotPositionName, + ReviewRequestStateName, + ReviewAssignmentStateName, + FormalLanguageName, + DocUrlTagName, + ExtResourceName, ) -from ietf.name.models import ( DocTypeName, DocTagName, StreamName, IntendedStdLevelName, StdLevelName, - DocRelationshipName, DocReminderTypeName, BallotPositionName, ReviewRequestStateName, ReviewAssignmentStateName, FormalLanguageName, - DocUrlTagName, ExtResourceName) from ietf.person.models import Email, Person from ietf.person.utils import get_active_balloters from ietf.utils import log @@ -48,24 +60,32 @@ from ietf.utils.mail import formataddr from ietf.utils.models import ForeignKey from ietf.utils.timezone import date_today, RPC_TZINFO, DEADLINE_TZINFO + if TYPE_CHECKING: # importing other than for type checking causes errors due to cyclic imports from ietf.meeting.models import ProceedingsMaterial, Session -logger = logging.getLogger('django') +logger = logging.getLogger("django") + class StateType(models.Model): - slug = models.CharField(primary_key=True, max_length=30) # draft, draft-iesg, charter, ... - label = models.CharField(max_length=255, help_text="Label that should be used (e.g. in admin) for state drop-down for this type of state") # State, IESG state, WG state, ... + slug = models.CharField( + primary_key=True, max_length=30 + ) # draft, draft-iesg, charter, ... + label = models.CharField( + max_length=255, + help_text="Label that should be used (e.g. in admin) for state drop-down for this type of state", + ) # State, IESG state, WG state, ... def __str__(self): return self.slug -@checks.register('db-consistency') + +@checks.register("db-consistency") def check_statetype_slugs(app_configs, **kwargs): errors = [] try: - state_type_slugs = [ t.slug for t in StateType.objects.all() ] + state_type_slugs = [t.slug for t in StateType.objects.all()] except django.db.ProgrammingError: # When running initial migrations on an empty DB, attempting to retrieve StateType will raise a # ProgrammingError. Until Django 3, there is no option to skip the checks. @@ -73,14 +93,19 @@ def check_statetype_slugs(app_configs, **kwargs): else: for type in DocTypeName.objects.all(): if not type.slug in state_type_slugs: - errors.append(checks.Error( - "The document type '%s (%s)' does not have a corresponding entry in the doc.StateType table" % (type.name, type.slug), - hint="You should add a doc.StateType entry with a slug '%s' to match the DocTypeName slug."%(type.slug), - obj=type, - id='datatracker.doc.E0015', - )) + errors.append( + checks.Error( + "The document type '%s (%s)' does not have a corresponding entry in the doc.StateType table" + % (type.name, type.slug), + hint="You should add a doc.StateType entry with a slug '%s' to match the DocTypeName slug." + % (type.slug), + obj=type, + id="datatracker.doc.E0015", + ) + ) return errors + class State(models.Model): type = ForeignKey(StateType) slug = models.SlugField() @@ -89,58 +114,97 @@ class State(models.Model): desc = models.TextField(blank=True) order = models.IntegerField(default=0) - next_states = models.ManyToManyField('doc.State', related_name="previous_states", blank=True) + next_states = models.ManyToManyField( + "doc.State", related_name="previous_states", blank=True + ) def __str__(self): return self.name - + class Meta: ordering = ["type", "order"] + IESG_BALLOT_ACTIVE_STATES = ("lc", "writeupw", "goaheadw", "iesg-eva", "defer") IESG_CHARTER_ACTIVE_STATES = ("intrev", "extrev", "iesgrev") IESG_STATCHG_CONFLREV_ACTIVE_STATES = ("iesgeval", "defer") -IESG_SUBSTATE_TAGS = ('ad-f-up', 'need-rev', 'extpty') +IESG_SUBSTATE_TAGS = ("ad-f-up", "need-rev", "extpty") + class DocumentInfo(models.Model): """Any kind of document. Draft, RFC, Charter, IPR Statement, Liaison Statement""" - time = models.DateTimeField(default=timezone.now) # should probably have auto_now=True - type = ForeignKey(DocTypeName, blank=True, null=True) # Draft, Agenda, Minutes, Charter, Discuss, Guideline, Email, Review, Issue, Wiki, External ... - title = models.CharField(max_length=255, validators=[validate_no_control_chars, ]) + time = models.DateTimeField( + default=timezone.now + ) # should probably have auto_now=True + + type = ForeignKey( + DocTypeName, blank=True, null=True + ) # Draft, Agenda, Minutes, Charter, Discuss, Guideline, Email, Review, Issue, Wiki, External ... + title = models.CharField( + max_length=255, + validators=[ + validate_no_control_chars, + ], + ) - states = models.ManyToManyField(State, blank=True) # plain state (Active/Expired/...), IESG state, stream state - tags = models.ManyToManyField(DocTagName, blank=True) # Revised ID Needed, ExternalParty, AD Followup, ... - stream = ForeignKey(StreamName, blank=True, null=True) # IETF, IAB, IRTF, Independent Submission, Editorial - group = ForeignKey(Group, blank=True, null=True) # WG, RG, IAB, IESG, Edu, Tools + states = models.ManyToManyField( + State, blank=True + ) # plain state (Active/Expired/...), IESG state, stream state + tags = models.ManyToManyField( + DocTagName, blank=True + ) # Revised ID Needed, ExternalParty, AD Followup, ... + stream = ForeignKey( + StreamName, blank=True, null=True + ) # IETF, IAB, IRTF, Independent Submission, Editorial + group = ForeignKey(Group, blank=True, null=True) # WG, RG, IAB, IESG, Edu, Tools abstract = models.TextField(blank=True) rev = models.CharField(verbose_name="revision", max_length=16, blank=True) pages = models.IntegerField(blank=True, null=True) words = models.IntegerField(blank=True, null=True) - formal_languages = models.ManyToManyField(FormalLanguageName, blank=True, help_text="Formal languages used in document") - intended_std_level = ForeignKey(IntendedStdLevelName, verbose_name="Intended standardization level", blank=True, null=True) - std_level = ForeignKey(StdLevelName, verbose_name="Standardization level", blank=True, null=True) - ad = ForeignKey(Person, verbose_name="area director", related_name='ad_%(class)s_set', blank=True, null=True) - shepherd = ForeignKey(Email, related_name='shepherd_%(class)s_set', blank=True, null=True) + formal_languages = models.ManyToManyField( + FormalLanguageName, blank=True, help_text="Formal languages used in document" + ) + intended_std_level = ForeignKey( + IntendedStdLevelName, + verbose_name="Intended standardization level", + blank=True, + null=True, + ) + std_level = ForeignKey( + StdLevelName, verbose_name="Standardization level", blank=True, null=True + ) + ad = ForeignKey( + Person, + verbose_name="area director", + related_name="ad_%(class)s_set", + blank=True, + null=True, + ) + shepherd = ForeignKey( + Email, related_name="shepherd_%(class)s_set", blank=True, null=True + ) expires = models.DateTimeField(blank=True, null=True) notify = models.TextField(max_length=1023, blank=True) external_url = models.URLField(blank=True) uploaded_filename = models.TextField(blank=True) note = models.TextField(blank=True) - rfc_number = models.PositiveIntegerField(blank=True, null=True) # only valid for type="rfc" + rfc_number = models.PositiveIntegerField( + blank=True, null=True + ) # only valid for type="rfc" def file_extension(self): - if not hasattr(self, '_cached_extension'): + if not hasattr(self, "_cached_extension"): if self.uploaded_filename: - _, ext= os.path.splitext(self.uploaded_filename) + _, ext = os.path.splitext(self.uploaded_filename) self._cached_extension = ext.lstrip(".").lower() else: self._cached_extension = "txt" return self._cached_extension def get_file_path(self): - if not hasattr(self, '_cached_file_path'): + if not hasattr(self, "_cached_file_path"): if self.type_id == "rfc": self._cached_file_path = settings.RFC_PATH elif self.type_id == "draft": @@ -148,63 +212,87 @@ def get_file_path(self): self._cached_file_path = settings.INTERNET_ALL_DRAFTS_ARCHIVE_DIR else: # This could be simplified since anything in INTERNET_DRAFT_PATH is also already in INTERNET_ALL_DRAFTS_ARCHIVE_DIR - draft_state = self.get_state('draft') - if draft_state and draft_state.slug == 'active': + draft_state = self.get_state("draft") + if draft_state and draft_state.slug == "active": self._cached_file_path = settings.INTERNET_DRAFT_PATH else: - self._cached_file_path = settings.INTERNET_ALL_DRAFTS_ARCHIVE_DIR + self._cached_file_path = ( + settings.INTERNET_ALL_DRAFTS_ARCHIVE_DIR + ) elif self.meeting_related() and self.type_id in ( - "agenda", "minutes", "narrativeminutes", "slides", "bluesheets", "procmaterials", "chatlog", "polls" + "agenda", + "minutes", + "narrativeminutes", + "slides", + "bluesheets", + "procmaterials", + "chatlog", + "polls", ): meeting = self.get_related_meeting() if meeting is not None: - self._cached_file_path = os.path.join(meeting.get_materials_path(), self.type_id) + "/" + self._cached_file_path = ( + os.path.join(meeting.get_materials_path(), self.type_id) + "/" + ) else: self._cached_file_path = "" elif self.type_id == "charter": self._cached_file_path = settings.CHARTER_PATH - elif self.type_id == "conflrev": + elif self.type_id == "conflrev": self._cached_file_path = settings.CONFLICT_REVIEW_PATH elif self.type_id == "statchg": self._cached_file_path = settings.STATUS_CHANGE_PATH - elif self.type_id == "bofreq": # TODO: This is probably unneeded, as is the separate path setting + elif ( + self.type_id == "bofreq" + ): # TODO: This is probably unneeded, as is the separate path setting self._cached_file_path = settings.BOFREQ_PATH else: self._cached_file_path = settings.DOCUMENT_PATH_PATTERN.format(doc=self) return self._cached_file_path def get_base_name(self): - if not hasattr(self, '_cached_base_name'): + if not hasattr(self, "_cached_base_name"): if self.uploaded_filename: self._cached_base_name = self.uploaded_filename - elif self.type_id == 'rfc': - self._cached_base_name = "%s.txt" % self.name - elif self.type_id == 'draft': + elif self.type_id == "rfc": + self._cached_base_name = "%s.txt" % self.name + elif self.type_id == "draft": if self.is_dochistory(): self._cached_base_name = "%s-%s.txt" % (self.doc.name, self.rev) else: self._cached_base_name = "%s-%s.txt" % (self.name, self.rev) - elif self.type_id in ["slides", "agenda", "minutes", "bluesheets", "procmaterials", ] and self.meeting_related(): - ext = 'pdf' if self.type_id == 'procmaterials' else 'txt' - self._cached_base_name = f'{self.name}-{self.rev}.{ext}' - elif self.type_id == 'review': + elif ( + self.type_id + in [ + "slides", + "agenda", + "minutes", + "bluesheets", + "procmaterials", + ] + and self.meeting_related() + ): + ext = "pdf" if self.type_id == "procmaterials" else "txt" + self._cached_base_name = f"{self.name}-{self.rev}.{ext}" + 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 - elif self.type_id in ['bofreq', 'statement']: + elif self.type_id in ["bofreq", "statement"]: self._cached_base_name = "%s-%s.md" % (self.name, self.rev) else: if self.rev: self._cached_base_name = "%s-%s.txt" % (self.name, self.rev) else: - self._cached_base_name = "%s.txt" % (self.name, ) + self._cached_base_name = "%s.txt" % (self.name,) return self._cached_base_name def get_file_name(self): - if not hasattr(self, '_cached_file_name'): - self._cached_file_name = os.path.join(self.get_file_path(), self.get_base_name()) + if not hasattr(self, "_cached_file_name"): + self._cached_file_name = os.path.join( + self.get_file_path(), self.get_base_name() + ) return self._cached_file_name - def revisions_by_dochistory(self): revisions = [] if self.type_id != "rfc": @@ -219,7 +307,7 @@ def revisions_by_newrevisionevent(self): revisions = [] if self.type_id != "rfc": doc = self.doc if isinstance(self, DocHistory) else self - for e in doc.docevent_set.filter(type='new_revision').distinct(): + for e in doc.docevent_set.filter(type="new_revision").distinct(): if e.rev and not e.rev in revisions: revisions.append(e.rev) if not doc.rev in revisions: @@ -228,25 +316,30 @@ def revisions_by_newrevisionevent(self): return revisions def get_href(self, meeting=None): - return self._get_ref(meeting=meeting,meeting_doc_refs=settings.MEETING_DOC_HREFS) - + return self._get_ref( + meeting=meeting, meeting_doc_refs=settings.MEETING_DOC_HREFS + ) def get_versionless_href(self, meeting=None): - return self._get_ref(meeting=meeting,meeting_doc_refs=settings.MEETING_DOC_GREFS) - + return self._get_ref( + meeting=meeting, meeting_doc_refs=settings.MEETING_DOC_GREFS + ) def _get_ref(self, meeting=None, meeting_doc_refs=settings.MEETING_DOC_HREFS): """ Returns an url to the document text. This differs from .get_absolute_url(), - which returns an url to the datatracker page for the document. + which returns an url to the datatracker page for the document. """ # If self.external_url truly is an url, use it. This is a change from # the earlier resolution order, but there's at the moment one single # instance which matches this (with correct results), so we won't # break things all over the place. - if not hasattr(self, '_cached_href'): + if not hasattr(self, "_cached_href"): validator = URLValidator() - if self.external_url and self.external_url.split(':')[0] in validator.schemes: + if ( + self.external_url + and self.external_url.split(":")[0] in validator.schemes + ): validator(self.external_url) return self.external_url @@ -260,7 +353,7 @@ def _get_ref(self, meeting=None, meeting_doc_refs=settings.MEETING_DOC_HREFS): elif self.type_id in settings.DOC_HREFS: self.is_meeting_related = False if self.type_id == "rfc": - format = settings.DOC_HREFS['rfc'] + format = settings.DOC_HREFS["rfc"] else: format = settings.DOC_HREFS[self.type_id] elif self.type_id in meeting_doc_refs: @@ -272,7 +365,7 @@ def _get_ref(self, meeting=None, meeting_doc_refs=settings.MEETING_DOC_HREFS): if not meeting: meeting = self.get_related_meeting() if meeting is None: - return '' + return "" # After IETF 96, meeting materials acquired revision # handling, and the document naming changed. @@ -290,8 +383,14 @@ def _get_ref(self, meeting=None, meeting_doc_refs=settings.MEETING_DOC_HREFS): # For slides that are not meeting-related, we need to know the file extension. # Assume we have access to the same files as settings.DOC_HREFS["slides"] and # see what extension is available - if self.type_id == "slides" and not self.meeting_related() and not href.endswith("/"): - filepath = Path(self.get_file_path()) / self.get_base_name() # start with this + if ( + self.type_id == "slides" + and not self.meeting_related() + and not href.endswith("/") + ): + filepath = ( + Path(self.get_file_path()) / self.get_base_name() + ) # start with this if not filepath.exists(): # Look for other extensions - grab the first one, sorted for stability for existing in sorted(filepath.parent.glob(f"{filepath.stem}.*")): @@ -299,7 +398,7 @@ def _get_ref(self, meeting=None, meeting_doc_refs=settings.MEETING_DOC_HREFS): break href += filepath.suffix # tack on the extension - if href.startswith('/'): + if href.startswith("/"): href = settings.IDTRACKER_BASE_URL + href self._cached_href = href return self._cached_href @@ -313,24 +412,30 @@ def set_state(self, state): self.states.remove(*others) if state not in already_set: self.states.add(state) - if state.type and state.type.slug == 'draft-iesg': - iesg_state = self.states.get(type_id="draft-iesg") # pyflakes:ignore - log.assertion('iesg_state', note="A document's 'draft-iesg' state should never be unset'. Failed for %s"%self.name) - self.state_cache = None # invalidate cache + if state.type and state.type.slug == "draft-iesg": + iesg_state = self.states.get(type_id="draft-iesg") # pyflakes:ignore + log.assertion( + "iesg_state", + note="A document's 'draft-iesg' state should never be unset'. Failed for %s" + % self.name, + ) + self.state_cache = None # invalidate cache self._cached_state_slug = {} def unset_state(self, state_type): """Unset state of type so no state of that type is any longer set.""" log.assertion('state_type != "draft-iesg"') self.states.remove(*self.states.filter(type=state_type)) - self.state_cache = None # invalidate cache + self.state_cache = None # invalidate cache self._cached_state_slug = {} def get_state(self, state_type=None): """Get state of type, or default state for document type if not specified. Uses a local cache to speed multiple state reads up.""" - if self.pk == None: # states is many-to-many so not in database implies no state + if ( + self.pk == None + ): # states is many-to-many so not in database implies no state return None if state_type == None: @@ -349,7 +454,7 @@ def get_state_slug(self, state_type=None): the slug of the state or None. This frees the caller of having to check against None before accessing the slug for a comparison.""" - if not hasattr(self, '_cached_state_slug'): + if not hasattr(self, "_cached_state_slug"): self._cached_state_slug = {} if not state_type in self._cached_state_slug: s = self.get_state(state_type) @@ -357,22 +462,28 @@ def get_state_slug(self, state_type=None): return self._cached_state_slug[state_type] def friendly_state(self): - """ Return a concise text description of the document's current state.""" + """Return a concise text description of the document's current state.""" state = self.get_state() if not state: return "Unknown state" - + if self.type_id == "rfc": return f"RFC {self.rfc_number} ({self.std_level})" - elif self.type_id == 'draft': + elif self.type_id == "draft": iesg_state = self.get_state("draft-iesg") iesg_state_summary = None if iesg_state: - iesg_substate = [t for t in self.tags.all() if t.slug in IESG_SUBSTATE_TAGS] + iesg_substate = [ + t for t in self.tags.all() if t.slug in IESG_SUBSTATE_TAGS + ] # There really shouldn't be more than one tag in iesg_substate, but this will do something sort-of-sensible if there is iesg_state_summary = iesg_state.name if iesg_substate: - iesg_state_summary = iesg_state_summary + "::"+"::".join(tag.name for tag in iesg_substate) + iesg_state_summary = ( + iesg_state_summary + + "::" + + "::".join(tag.name for tag in iesg_substate) + ) rfc = self.became_rfc() if rfc: @@ -381,21 +492,40 @@ def friendly_state(self): elif state.slug == "repl": rs = self.related_that("replaces") if rs: - return mark_safe("Replaced by " + ", ".join("%s" % (urlreverse('ietf.doc.views_doc.document_main', kwargs=dict(name=related.name)), related) for related in rs)) + return mark_safe( + "Replaced by " + + ", ".join( + '%s' + % ( + urlreverse( + "ietf.doc.views_doc.document_main", + kwargs=dict(name=related.name), + ), + related, + ) + for related in rs + ) + ) else: return "Replaced" elif state.slug == "active": if iesg_state: if iesg_state.slug == "dead": # Many drafts in the draft-iesg "Dead" state are not dead - # in other state machines; they're just not currently under + # in other state machines; they're just not currently under # IESG processing. Show them as "I-D Exists (IESG: Dead)" instead... return "I-D Exists (IESG: %s)" % iesg_state_summary elif iesg_state.slug == "lc": e = self.latest_event(LastCallDocEvent, type="sent_last_call") if e: - return iesg_state_summary + " (ends %s)" % e.expires.astimezone(DEADLINE_TZINFO).date().isoformat() - + return ( + iesg_state_summary + + " (ends %s)" + % e.expires.astimezone(DEADLINE_TZINFO) + .date() + .isoformat() + ) + return iesg_state_summary else: return "I-D Exists" @@ -418,7 +548,7 @@ def author_list(self): return ", ".join(best_addresses) def authors(self): - return [ a.person for a in self.documentauthor_set.all() ] + return [a.person for a in self.documentauthor_set.all()] # This, and several other ballot related functions here, assume that there is only one active ballot for a document at any point in time. # If that assumption is violated, they will only expose the most recently created ballot @@ -428,7 +558,9 @@ def ballot_open(self, ballot_type_slug): def latest_ballot(self): """Returns the most recently created ballot""" - ballot = self.latest_event(BallotDocEvent, type__in=("created_ballot", "closed_ballot")) + ballot = self.latest_event( + BallotDocEvent, type__in=("created_ballot", "closed_ballot") + ) return ballot def active_ballot(self): @@ -444,25 +576,42 @@ def has_rfc_editor_note(self): return e != None and (e.text != "") def meeting_related(self): - if self.type_id in ("agenda","minutes", "narrativeminutes", "bluesheets","slides","recording","procmaterials","chatlog","polls"): - return self.type_id != "slides" or self.get_state_slug('reuse_policy')=='single' + if self.type_id in ( + "agenda", + "minutes", + "narrativeminutes", + "bluesheets", + "slides", + "recording", + "procmaterials", + "chatlog", + "polls", + ): + return ( + self.type_id != "slides" + or self.get_state_slug("reuse_policy") == "single" + ) return False - def get_related_session(self) -> Optional['Session']: + def get_related_session(self) -> Optional["Session"]: """Get the meeting session related to this document Return None if there is no related session. Must define this in DocumentInfo subclasses. """ - raise NotImplementedError(f'Class {self.__class__} must define get_related_session()') + raise NotImplementedError( + f"Class {self.__class__} must define get_related_session()" + ) - def get_related_proceedings_material(self) -> Optional['ProceedingsMaterial']: + def get_related_proceedings_material(self) -> Optional["ProceedingsMaterial"]: """Get the proceedings material related to this document Return None if there is no related proceedings material. Must define this in DocumentInfo subclasses. """ - raise NotImplementedError(f'Class {self.__class__} must define get_related_proceedings_material()') + raise NotImplementedError( + f"Class {self.__class__} must define get_related_proceedings_material()" + ) def get_related_meeting(self): """Get the meeting this document relates to""" @@ -470,18 +619,24 @@ def get_related_meeting(self): return None # no related meeting if not meeting_related! # get an item that links this doc to a meeting item = self.get_related_session() or self.get_related_proceedings_material() - return getattr(item, 'meeting', None) + return getattr(item, "meeting", None) def relations_that(self, relationship): """Return the related-document objects that describe a given relationship targeting self.""" if isinstance(relationship, str): - relationship = ( relationship, ) + relationship = (relationship,) if not isinstance(relationship, tuple): - raise TypeError("Expected a string or tuple, received %s" % type(relationship)) + raise TypeError( + "Expected a string or tuple, received %s" % type(relationship) + ) if isinstance(self, Document): - return RelatedDocument.objects.filter(target=self, relationship__in=relationship).select_related('source') + return RelatedDocument.objects.filter( + target=self, relationship__in=relationship + ).select_related("source") elif isinstance(self, DocHistory): - return RelatedDocHistory.objects.filter(target=self.doc, relationship__in=relationship).select_related('source') + return RelatedDocHistory.objects.filter( + target=self.doc, relationship__in=relationship + ).select_related("source") else: raise TypeError("Expected method called on Document or DocHistory") @@ -491,20 +646,26 @@ def all_relations_that(self, relationship, related=None): rels = self.relations_that(relationship) for r in rels: if not r in related: - related += ( r, ) + related += (r,) related = r.source.all_relations_that(relationship, related) return related def relations_that_doc(self, relationship): """Return the related-document objects that describe a given relationship from self to other documents.""" if isinstance(relationship, str): - relationship = ( relationship, ) + relationship = (relationship,) if not isinstance(relationship, tuple): - raise TypeError("Expected a string or tuple, received %s" % type(relationship)) + raise TypeError( + "Expected a string or tuple, received %s" % type(relationship) + ) if isinstance(self, Document): - return RelatedDocument.objects.filter(source=self, relationship__in=relationship).select_related('target') + return RelatedDocument.objects.filter( + source=self, relationship__in=relationship + ).select_related("target") elif isinstance(self, DocHistory): - return RelatedDocHistory.objects.filter(source=self, relationship__in=relationship).select_related('target') + return RelatedDocHistory.objects.filter( + source=self, relationship__in=relationship + ).select_related("target") else: raise TypeError("Expected method called on Document or DocHistory") @@ -514,7 +675,7 @@ def all_relations_that_doc(self, relationship, related=None): rels = self.relations_that_doc(relationship) for r in rels: if not r in related: - related += ( r, ) + related += (r,) related = r.target.all_relations_that_doc(relationship, related) return related @@ -534,46 +695,46 @@ def replaces(self): return self.related_that_doc("replaces") def replaced_by(self): - return set([ r.document for r in self.related_that("replaces") ]) + return set([r.document for r in self.related_that("replaces")]) def _text_path(self): path = self.get_file_name() - root, ext = os.path.splitext(path) - txtpath = root+'.txt' - if ext != '.txt' and os.path.exists(txtpath): + root, ext = os.path.splitext(path) + txtpath = root + ".txt" + if ext != ".txt" and os.path.exists(txtpath): path = txtpath return path - + def text_exists(self): path = Path(self._text_path()) return path.exists() - def text(self, size = -1): + def text(self, size=-1): path = Path(self._text_path()) if not path.exists(): return None try: - with path.open('rb') as file: + with path.open("rb") as file: raw = file.read(size) except IOError as e: log.log(f"Error reading text for {path}: {e}") return None text = None try: - text = raw.decode('utf-8') + text = raw.decode("utf-8") except UnicodeDecodeError: - for back in range(1,4): + for back in range(1, 4): try: - text = raw[:-back].decode('utf-8') + text = raw[:-back].decode("utf-8") break except UnicodeDecodeError: pass if text is None: - text = raw.decode('latin-1') + text = raw.decode("latin-1") return text def text_or_error(self): - return self.text() or "Error; cannot read '%s'"%self.get_base_name() + return self.text() or "Error; cannot read '%s'" % self.get_base_name() def html_body(self, classes=""): if self.type_id == "rfc": @@ -621,14 +782,14 @@ def html_body(self, classes=""): def htmlized(self): name = self.get_base_name() text = self.text() - if name.endswith('.html'): + if name.endswith(".html"): return text - if not name.endswith('.txt'): + if not name.endswith(".txt"): return None html = "" if text: - cache = caches['htmlized'] - cache_key = name.split('.')[0] + cache = caches["htmlized"] + cache_key = name.split(".")[0] try: html = cache.get(cache_key) except EOFError: @@ -650,7 +811,9 @@ def pdfized(self): stylesheets.append(finders.find("ietf/css/document_html_txt.css")) else: text = self.htmlized() - stylesheets.append(f'{settings.STATIC_IETF_ORG_INTERNAL}/fonts/noto-sans-mono/import.css') + stylesheets.append( + f"{settings.STATIC_IETF_ORG_INTERNAL}/fonts/noto-sans-mono/import.css" + ) cache = caches["pdfized"] cache_key = name.split(".")[0] @@ -672,25 +835,29 @@ def pdfized(self): except AssertionError: pdf = None except Exception as e: - log.log('weasyprint failed:'+str(e)) + log.log("weasyprint failed:" + str(e)) raise if pdf: cache.set(cache_key, pdf, settings.PDFIZER_CACHE_TIME) return pdf def references(self): - return self.relations_that_doc(('refnorm','refinfo','refunk','refold')) + return self.relations_that_doc(("refnorm", "refinfo", "refunk", "refold")) def referenced_by(self): - return self.relations_that(("refnorm", "refinfo", "refunk", "refold")).filter( - models.Q( - source__type__slug="draft", - source__states__type__slug="draft", - source__states__slug="active", + return ( + self.relations_that(("refnorm", "refinfo", "refunk", "refold")) + .filter( + models.Q( + source__type__slug="draft", + source__states__type__slug="draft", + source__states__slug="active", + ) + | models.Q(source__type__slug="rfc") ) - | models.Q(source__type__slug="rfc") - ).distinct() - + .distinct() + ) + def referenced_by_rfcs(self): """Get refs to this doc from RFCs""" return self.relations_that(("refnorm", "refinfo", "refunk", "refold")).filter( @@ -700,18 +867,22 @@ def referenced_by_rfcs(self): def became_rfc(self): if not hasattr(self, "_cached_became_rfc"): doc = self if isinstance(self, Document) else self.doc - self._cached_became_rfc = next(iter(doc.related_that_doc("became_rfc")), None) + self._cached_became_rfc = next( + iter(doc.related_that_doc("became_rfc")), None + ) return self._cached_became_rfc def came_from_draft(self): if not hasattr(self, "_cached_came_from_draft"): doc = self if isinstance(self, Document) else self.doc - self._cached_came_from_draft = next(iter(doc.related_that("became_rfc")), None) + self._cached_came_from_draft = next( + iter(doc.related_that("became_rfc")), None + ) return self._cached_came_from_draft - + def contains(self): return self.related_that_doc("contains") - + def part_of(self): return self.related_that("contains") @@ -721,13 +892,14 @@ def referenced_by_rfcs_as_rfc_or_draft(self): if self.type_id == "rfc" and self.came_from_draft(): refs_to |= self.came_from_draft().referenced_by_rfcs() return refs_to - + class Meta: abstract = True class HasNameRevAndTypeIdProtocol(Protocol): """Typing Protocol describing a class that has name, rev, and type_id properties""" + @property def name(self) -> str: ... @property @@ -738,13 +910,16 @@ def type_id(self) -> str: ... class StorableMixin: """Mixin that adds storage helpers to a DocumentInfo subclass""" + def store_str( self: HasNameRevAndTypeIdProtocol, name: str, content: str, - allow_overwrite: bool = False + allow_overwrite: bool = False, ) -> None: - return utils_store_str(self.type_id, name, content, allow_overwrite, self.name, self.rev) + return utils_store_str( + self.type_id, name, content, allow_overwrite, self.name, self.rev + ) def store_bytes( self: HasNameRevAndTypeIdProtocol, @@ -752,9 +927,11 @@ def store_bytes( content: bytes, allow_overwrite: bool = False, doc_name: Optional[str] = None, - doc_rev: Optional[str] = None + doc_rev: Optional[str] = None, ) -> None: - return utils_store_bytes(self.type_id, name, content, allow_overwrite, self.name, self.rev) + return utils_store_bytes( + self.type_id, name, content, allow_overwrite, self.name, self.rev + ) def store_file( self: HasNameRevAndTypeIdProtocol, @@ -762,25 +939,37 @@ def store_file( file: Union[File, BufferedReader], allow_overwrite: bool = False, doc_name: Optional[str] = None, - doc_rev: Optional[str] = None + doc_rev: Optional[str] = None, ) -> None: - return utils_store_file(self.type_id, name, file, allow_overwrite, self.name, self.rev) + return utils_store_file( + self.type_id, name, file, allow_overwrite, self.name, self.rev + ) -STATUSCHANGE_RELATIONS = ('tops','tois','tohist','toinf','tobcp','toexp') +STATUSCHANGE_RELATIONS = ("tops", "tois", "tohist", "toinf", "tobcp", "toexp") + class RelatedDocument(models.Model): - source = ForeignKey('Document') - target = ForeignKey('Document', related_name='targets_related') + source = ForeignKey("Document") + target = ForeignKey("Document", related_name="targets_related") relationship = ForeignKey(DocRelationshipName) originaltargetaliasname = models.CharField(max_length=255, null=True, blank=True) + def action(self): return self.relationship.name + def __str__(self): - return u"%s %s %s" % (self.source.name, self.relationship.name.lower(), self.target.name) + return "%s %s %s" % ( + self.source.name, + self.relationship.name.lower(), + self.target.name, + ) def is_downref(self): - if self.source.type_id not in ["draft","rfc"] or self.relationship.slug not in [ + if self.source.type_id not in [ + "draft", + "rfc", + ] or self.relationship.slug not in [ "refnorm", "refold", "refunk", @@ -789,7 +978,7 @@ def is_downref(self): if self.source.type_id == "rfc": source_lvl = self.source.std_level_id - elif self.source.type_id in ["bcp","std"]: + elif self.source.type_id in ["bcp", "std"]: source_lvl = self.source.type_id else: source_lvl = self.source.intended_std_level_id @@ -797,16 +986,16 @@ def is_downref(self): if source_lvl not in ["bcp", "ps", "ds", "std", "unkn"]: return None - if self.target.type_id == 'rfc': + if self.target.type_id == "rfc": if not self.target.std_level: - target_lvl = 'unkn' + target_lvl = "unkn" else: target_lvl = self.target.std_level_id elif self.target.type_id in ["bcp", "std"]: target_lvl = self.target.type_id else: if not self.target.intended_std_level: - target_lvl = 'unkn' + target_lvl = "unkn" else: target_lvl = self.target.intended_std_level_id @@ -839,18 +1028,32 @@ def is_downref(self): def is_approved_downref(self): - if self.target.type_id == 'rfc': - if RelatedDocument.objects.filter(relationship_id='downref-approval', target=self.target).exists(): - return "Approved Downref" + if self.target.type_id == "rfc": + if RelatedDocument.objects.filter( + relationship_id="downref-approval", target=self.target + ).exists(): + return "Approved Downref" return False + class DocumentAuthorInfo(models.Model): person = ForeignKey(Person) # email should only be null for some historic documents - email = ForeignKey(Email, help_text="Email address used by author for submission", blank=True, null=True) - affiliation = models.CharField(max_length=100, blank=True, help_text="Organization/company used by author for submission") - country = models.CharField(max_length=255, blank=True, help_text="Country used by author for submission") + email = ForeignKey( + Email, + help_text="Email address used by author for submission", + blank=True, + null=True, + ) + affiliation = models.CharField( + max_length=100, + blank=True, + help_text="Organization/company used by author for submission", + ) + country = models.CharField( + max_length=255, blank=True, help_text="Country used by author for submission" + ) order = models.IntegerField(default=1) def formatted_email(self): @@ -864,65 +1067,87 @@ class Meta: abstract = True ordering = ["document", "order"] indexes = [ - models.Index(fields=['document', 'order']), + models.Index(fields=["document", "order"]), ] + class DocumentAuthor(DocumentAuthorInfo): - document = ForeignKey('Document') + document = ForeignKey("Document") def __str__(self): - return u"%s %s (%s)" % (self.document.name, self.person, self.order) + return "%s %s (%s)" % (self.document.name, self.person, self.order) class DocumentActionHolder(models.Model): """Action holder for a document""" - document = ForeignKey('Document') + + document = ForeignKey("Document") person = ForeignKey(Person) time_added = models.DateTimeField(default=timezone.now) - CLEAR_ACTION_HOLDERS_STATES = ['approved', 'ann', 'rfcqueue', 'pub', 'dead'] # draft-iesg state slugs - GROUP_ROLES_OF_INTEREST = ['chair', 'techadv', 'editor', 'secr'] + CLEAR_ACTION_HOLDERS_STATES = [ + "approved", + "ann", + "rfcqueue", + "pub", + "dead", + ] # draft-iesg state slugs + GROUP_ROLES_OF_INTEREST = ["chair", "techadv", "editor", "secr"] def __str__(self): return str(self.person) class Meta: constraints = [ - models.UniqueConstraint(fields=['document', 'person'], name='unique_action_holder') + models.UniqueConstraint( + fields=["document", "person"], name="unique_action_holder" + ) ] def role_for_doc(self): """Brief string description of this person's relationship to the doc""" roles = [] if self.person in self.document.authors(): - roles.append('Author') + roles.append("Author") if self.person == self.document.ad: - roles.append('Responsible AD') + roles.append("Responsible AD") if self.document.shepherd and self.person == self.document.shepherd.person: - roles.append('Shepherd') + roles.append("Shepherd") if self.document.group: - roles.extend([ - 'Group %s' % role.name.name - for role in self.document.group.role_set.filter( - name__in=self.GROUP_ROLES_OF_INTEREST, - person=self.person, - ) - ]) + roles.extend( + [ + "Group %s" % role.name.name + for role in self.document.group.role_set.filter( + name__in=self.GROUP_ROLES_OF_INTEREST, + person=self.person, + ) + ] + ) if not roles: - roles.append('Action Holder') - return ', '.join(roles) + roles.append("Action Holder") + return ", ".join(roles) + validate_docname = RegexValidator( - r'^[-a-z0-9]+$', + r"^[-a-z0-9]+$", "Provide a valid document name consisting of lowercase letters, numbers and hyphens.", - 'invalid' + "invalid", ) + class Document(StorableMixin, DocumentInfo): - name = models.CharField(max_length=255, validators=[validate_docname,], unique=True) # immutable - - action_holders = models.ManyToManyField(Person, through=DocumentActionHolder, blank=True) + name = models.CharField( + max_length=255, + validators=[ + validate_docname, + ], + unique=True, + ) # immutable + + action_holders = models.ManyToManyField( + Person, through=DocumentActionHolder, blank=True + ) def __str__(self): return self.name @@ -932,23 +1157,36 @@ def get_absolute_url(self): Returns an url to the document view. This differs from .get_href(), which returns an url to the document content. """ - if not hasattr(self, '_cached_absolute_url'): + if not hasattr(self, "_cached_absolute_url"): name = self.name url = None if self.type_id == "draft" and self.get_state_slug() == "rfc": name = self.name - url = urlreverse('ietf.doc.views_doc.document_main', kwargs={ 'name': name }, urlconf="ietf.urls") - elif self.type_id in ('slides','bluesheets','recording'): + url = urlreverse( + "ietf.doc.views_doc.document_main", + kwargs={"name": name}, + urlconf="ietf.urls", + ) + elif self.type_id in ("slides", "bluesheets", "recording"): session = self.session_set.first() if session: meeting = session.meeting - if self.type_id == 'recording': + if self.type_id == "recording": url = self.external_url else: filename = self.uploaded_filename - url = '%sproceedings/%s/%s/%s' % (settings.IETF_HOST_URL,meeting.number,self.type_id,filename) + url = "%sproceedings/%s/%s/%s" % ( + settings.IETF_HOST_URL, + meeting.number, + self.type_id, + filename, + ) else: - url = urlreverse('ietf.doc.views_doc.document_main', kwargs={ 'name': name }, urlconf="ietf.urls") + url = urlreverse( + "ietf.doc.views_doc.document_main", + kwargs={"name": name}, + urlconf="ietf.urls", + ) self._cached_absolute_url = url return self._cached_absolute_url @@ -964,19 +1202,24 @@ def file_tag(self): def filename_with_rev(self): return "%s-%s.txt" % (self.name, self.rev) - + def latest_event(self, *args, **filter_args): """Get latest event of optional Python type and with filter arguments, e.g. d.latest_event(type="xyz") returns a DocEvent while d.latest_event(WriteupDocEvent, type="xyz") returns a WriteupDocEvent event.""" model = args[0] if args else DocEvent - e = model.objects.filter(doc=self).filter(**filter_args).order_by('-time', '-id').first() + e = ( + model.objects.filter(doc=self) + .filter(**filter_args) + .order_by("-time", "-id") + .first() + ) return e def display_name(self): name = self.name - if name.startswith('rfc'): + if name.startswith("rfc"): name = name.upper() return name @@ -985,7 +1228,9 @@ def save_with_history(self, events): can be retrieved later. You must pass in at least one event with a description of what happened.""" - assert events, "You must always add at least one event to describe the changes in the history log" + assert ( + events + ), "You must always add at least one event to describe the changes in the history log" self.time = max(self.time, events[0].time) self._has_an_event_so_saving_is_allowed = True @@ -993,24 +1238,39 @@ def save_with_history(self, events): del self._has_an_event_so_saving_is_allowed from ietf.doc.utils import save_document_in_history + save_document_in_history(self) def save(self, *args, **kwargs): # if there's no primary key yet, we can allow the save to go # through to break the cycle between the document and any # events - assert kwargs.get("force_insert", False) or getattr(self, "_has_an_event_so_saving_is_allowed", None), "Use .save_with_history to save documents" + assert kwargs.get("force_insert", False) or getattr( + self, "_has_an_event_so_saving_is_allowed", None + ), "Use .save_with_history to save documents" super(Document, self).save(*args, **kwargs) def telechat_date(self, e=None): if not e: e = self.latest_event(TelechatDocEvent, type="scheduled_for_telechat") - return e.telechat_date if e and e.telechat_date and e.telechat_date >= date_today(settings.TIME_ZONE) else None + return ( + e.telechat_date + if e + and e.telechat_date + and e.telechat_date >= date_today(settings.TIME_ZONE) + else None + ) def past_telechat_date(self): "Return the latest telechat date if it isn't in the future; else None" e = self.latest_event(TelechatDocEvent, type="scheduled_for_telechat") - return e.telechat_date if e and e.telechat_date and e.telechat_date < date_today(settings.TIME_ZONE) else None + return ( + e.telechat_date + if e + and e.telechat_date + and e.telechat_date < date_today(settings.TIME_ZONE) + else None + ) def previous_telechat_date(self): "Return the most recent telechat date in the past, if any (even if there's another in the future)" @@ -1022,7 +1282,11 @@ def previous_telechat_date(self): return e.telechat_date if e else None def request_closed_time(self, review_req): - e = self.latest_event(ReviewRequestDocEvent, type="closed_review_request", review_request=review_req) + e = self.latest_event( + ReviewRequestDocEvent, + type="closed_review_request", + review_request=review_req, + ) return e.time if e and e.time else None def area_acronym(self): @@ -1034,7 +1298,7 @@ def area_acronym(self): return g.parent.acronym else: return None - + def group_acronym(self): g = self.group if g and g.type_id != "area": @@ -1054,21 +1318,32 @@ def returning_item(self): # isn't this just returning whether the state is currently a defer state for that document type? def active_defer_event(self): if self.type_id == "draft" and self.get_state_slug("draft-iesg") == "defer": - return self.latest_event(type="changed_state", desc__icontains="State changed to IESG Evaluation - Defer") + return self.latest_event( + type="changed_state", + desc__icontains="State changed to IESG Evaluation - Defer", + ) elif self.type_id == "conflrev" and self.get_state_slug("conflrev") == "defer": - return self.latest_event(type="changed_state", desc__icontains="State changed to IESG Evaluation - Defer") + return self.latest_event( + type="changed_state", + desc__icontains="State changed to IESG Evaluation - Defer", + ) elif self.type_id == "statchg" and self.get_state_slug("statchg") == "defer": - return self.latest_event(type="changed_state", desc__icontains="State changed to IESG Evaluation - Defer") + return self.latest_event( + type="changed_state", + desc__icontains="State changed to IESG Evaluation - Defer", + ) return None def most_recent_ietflc(self): """Returns the most recent IETF LastCallDocEvent for this document""" - return self.latest_event(LastCallDocEvent,type="sent_last_call") + return self.latest_event(LastCallDocEvent, type="sent_last_call") def displayname_with_link(self): - return mark_safe('%s-%s' % (self.get_absolute_url(), self.name , self.rev)) + return mark_safe( + '%s-%s' % (self.get_absolute_url(), self.name, self.rev) + ) - def ipr(self,states=settings.PUBLISH_IPR_STATES): + def ipr(self, states=settings.PUBLISH_IPR_STATES): """Returns the IPR disclosures against this document (as a queryset over IprDocRel).""" # from ietf.ipr.models import IprDocRel # return IprDocRel.objects.filter(document__docs=self, disclosure__state__in=states) # TODO - clear these comments away @@ -1079,10 +1354,10 @@ def related_ipr(self): document directly or indirectly obsoletes or replaces """ from ietf.ipr.models import IprDocRel + iprs = ( IprDocRel.objects.filter( - document__in=[self] - + self.all_related_that_doc(("obs", "replaces")) + document__in=[self] + self.all_related_that_doc(("obs", "replaces")) ) .filter(disclosure__state__in=settings.PUBLISH_IPR_STATES) .values_list("disclosure", flat=True) @@ -1090,27 +1365,39 @@ def related_ipr(self): ) return iprs - def future_presentations(self): - """ returns related SessionPresentation objects for meetings that - have not yet ended. This implementation allows for 2 week meetings """ + """returns related SessionPresentation objects for meetings that + have not yet ended. This implementation allows for 2 week meetings""" candidate_presentations = self.presentations.filter( session__meeting__date__gte=date_today() - datetime.timedelta(days=15) ) return sorted( - [pres for pres in candidate_presentations - if pres.session.meeting.end_date() >= date_today()], - key=lambda x:x.session.meeting.date, + [ + pres + for pres in candidate_presentations + if pres.session.meeting.end_date() >= date_today() + ], + key=lambda x: x.session.meeting.date, ) def last_presented(self): - """ returns related SessionPresentation objects for the most recent meeting in the past""" + """returns related SessionPresentation objects for the most recent meeting in the past""" # Assumes no two meetings have the same start date - if the assumption is violated, one will be chosen arbitrarily today = date_today() - candidate_presentations = self.presentations.filter(session__meeting__date__lte=today) - candidate_meetings = set([p.session.meeting for p in candidate_presentations if p.session.meeting.end_date()= (e.time, e.pk) def get_dochistory(self): - return DocHistory.objects.filter(time__lte=self.time,doc__name=self.doc.name).order_by('-time', '-pk').first() + return ( + DocHistory.objects.filter(time__lte=self.time, doc__name=self.doc.name) + .order_by("-time", "-pk") + .first() + ) def __str__(self): - return u"%s %s by %s at %s" % (self.doc.name, self.get_type_display().lower(), self.by.plain_name(), self.time) - + return "%s %s by %s at %s" % ( + self.doc.name, + self.get_type_display().lower(), + self.by.plain_name(), + self.time, + ) + class Meta: - ordering = ['-time', '-id'] + ordering = ["-time", "-id"] indexes = [ - models.Index(fields=['type', 'doc']), - models.Index(fields=['-time', '-id']), + models.Index(fields=["type", "doc"]), + models.Index(fields=["-time", "-id"]), ] - + + class NewRevisionDocEvent(DocEvent): pass + class IanaExpertDocEvent(DocEvent): pass + class StateDocEvent(DocEvent): state_type = ForeignKey(StateType) state = ForeignKey(State, blank=True, null=True) + class ConsensusDocEvent(DocEvent): consensus = models.BooleanField(null=True, default=None) + # IESG events class BallotType(models.Model): doc_type = ForeignKey(DocTypeName, blank=True, null=True) @@ -1436,10 +1750,11 @@ class BallotType(models.Model): positions = models.ManyToManyField(BallotPositionName, blank=True) def __str__(self): - return u"%s: %s" % (self.name, self.doc_type.name) - + return "%s: %s" % (self.name, self.doc_type.name) + class Meta: - ordering = ['order'] + ordering = ["order"] + class BallotDocEvent(DocEvent): ballot_type = ForeignKey(BallotType) @@ -1447,9 +1762,17 @@ class BallotDocEvent(DocEvent): def active_balloter_positions(self): """Return dict mapping each active member of the balloting body to a current ballot position (or None if they haven't voted).""" res = {} - + active_balloters = get_active_balloters(self.ballot_type) - positions = BallotPositionDocEvent.objects.filter(type="changed_ballot_position",balloter__in=active_balloters, ballot=self).select_related('balloter', 'pos').order_by("-time", "-id") + positions = ( + BallotPositionDocEvent.objects.filter( + type="changed_ballot_position", + balloter__in=active_balloters, + ballot=self, + ) + .select_related("balloter", "pos") + .order_by("-time", "-id") + ) for pos in positions: if pos.balloter not in res: @@ -1466,7 +1789,13 @@ def all_positions(self): positions = [] seen = {} active_balloters = get_active_balloters(self.ballot_type) - for e in BallotPositionDocEvent.objects.filter(type="changed_ballot_position", ballot=self).select_related('balloter', 'pos').order_by("-time", '-id'): + for e in ( + BallotPositionDocEvent.objects.filter( + type="changed_ballot_position", ballot=self + ) + .select_related("balloter", "pos") + .order_by("-time", "-id") + ): if e.balloter not in seen: e.is_old_pos = e.balloter not in active_balloters e.old_positions = [] @@ -1478,7 +1807,7 @@ def all_positions(self): prev = latest.old_positions[-1] else: prev = latest.pos - + if e.pos != prev: latest.old_positions.append(e.pos) @@ -1493,7 +1822,12 @@ def all_positions(self): norecord = BallotPositionName.objects.get(slug="norecord") for balloter in active_balloters: if balloter not in seen: - e = BallotPositionDocEvent(type="changed_ballot_position", doc=self.doc, rev=self.doc.rev, balloter=balloter) + e = BallotPositionDocEvent( + type="changed_ballot_position", + doc=self.doc, + rev=self.doc.rev, + balloter=balloter, + ) e.by = balloter e.pos = norecord e.is_old_pos = False @@ -1503,17 +1837,27 @@ def all_positions(self): positions.sort(key=lambda p: (p.is_old_pos, p.balloter.last_name())) return positions + class IRSGBallotDocEvent(BallotDocEvent): duedate = models.DateTimeField(blank=True, null=True) + class BallotPositionDocEvent(DocEvent): - ballot = ForeignKey(BallotDocEvent, null=True, default=None) # default=None is a temporary migration period fix, should be removed when charter branch is live + ballot = ForeignKey( + BallotDocEvent, null=True, default=None + ) # default=None is a temporary migration period fix, should be removed when charter branch is live balloter = ForeignKey(Person) pos = ForeignKey(BallotPositionName, verbose_name="position", default="norecord") - discuss = models.TextField(help_text="Discuss text if position is discuss", blank=True) - discuss_time = models.DateTimeField(help_text="Time discuss text was written", blank=True, null=True) + discuss = models.TextField( + help_text="Discuss text if position is discuss", blank=True + ) + discuss_time = models.DateTimeField( + help_text="Time discuss text was written", blank=True, null=True + ) comment = models.TextField(help_text="Optional comment", blank=True) - comment_time = models.DateTimeField(help_text="Time optional comment was written", blank=True, null=True) + comment_time = models.DateTimeField( + help_text="Time optional comment was written", blank=True, null=True + ) send_email = models.BooleanField(null=True, default=None) @memoize @@ -1524,85 +1868,120 @@ def any_email_sent(self): ballot=self.ballot, time__lte=self.time, balloter=self.balloter, - ).values_list('send_email', flat=True) - false = any( s==False for s in sent_list ) - true = any( s==True for s in sent_list ) + ).values_list("send_email", flat=True) + false = any(s == False for s in sent_list) + true = any(s == True for s in sent_list) return True if true else False if false else None class WriteupDocEvent(DocEvent): text = models.TextField(blank=True) + class LastCallDocEvent(DocEvent): expires = models.DateTimeField(blank=True, null=True) - + + class TelechatDocEvent(DocEvent): telechat_date = models.DateField(blank=True, null=True) returning_item = models.BooleanField(default=False) + class ReviewRequestDocEvent(DocEvent): - review_request = ForeignKey('review.ReviewRequest') + review_request = ForeignKey("review.ReviewRequest") state = ForeignKey(ReviewRequestStateName, blank=True, null=True) + class ReviewAssignmentDocEvent(DocEvent): - review_assignment = ForeignKey('review.ReviewAssignment') + review_assignment = ForeignKey("review.ReviewAssignment") state = ForeignKey(ReviewAssignmentStateName, blank=True, null=True) + # charter events class InitialReviewDocEvent(DocEvent): expires = models.DateTimeField(blank=True, null=True) + class AddedMessageEvent(DocEvent): import ietf.message.models - message = ForeignKey(ietf.message.models.Message, null=True, blank=True,related_name='doc_manualevents') - msgtype = models.CharField(max_length=25) - in_reply_to = ForeignKey(ietf.message.models.Message, null=True, blank=True,related_name='doc_irtomanual') + + message = ForeignKey( + ietf.message.models.Message, + null=True, + blank=True, + related_name="doc_manualevents", + ) + msgtype = models.CharField(max_length=25) + in_reply_to = ForeignKey( + ietf.message.models.Message, + null=True, + blank=True, + related_name="doc_irtomanual", + ) class SubmissionDocEvent(DocEvent): import ietf.submit.models + submission = ForeignKey(ietf.submit.models.Submission) + # dumping store for removed events class DeletedEvent(models.Model): content_type = ForeignKey(ContentType) - json = models.TextField(help_text="Deleted object in JSON format, with attribute names chosen to be suitable for passing into the relevant create method.") + json = models.TextField( + help_text="Deleted object in JSON format, with attribute names chosen to be suitable for passing into the relevant create method." + ) by = ForeignKey(Person) time = models.DateTimeField(default=timezone.now) def __str__(self): - return u"%s by %s %s" % (self.content_type, self.by, self.time) + return "%s by %s %s" % (self.content_type, self.by, self.time) + class EditedAuthorsDocEvent(DocEvent): - """ Capture the reasoning or authority for changing a document author list. - Allows programs to recognize and not change lists that have been manually verified and corrected. - Example 'basis' values might be from ['manually adjusted','recomputed by parsing document', etc.] + """Capture the reasoning or authority for changing a document author list. + Allows programs to recognize and not change lists that have been manually verified and corrected. + Example 'basis' values might be from ['manually adjusted','recomputed by parsing document', etc.] """ - basis = models.CharField(help_text="What is the source or reasoning for the changes to the author list",max_length=255) + + basis = models.CharField( + help_text="What is the source or reasoning for the changes to the author list", + max_length=255, + ) + class BofreqEditorDocEvent(DocEvent): - """ Capture the proponents of a BOF Request.""" - editors = models.ManyToManyField('person.Person', blank=True) + """Capture the proponents of a BOF Request.""" + + editors = models.ManyToManyField("person.Person", blank=True) + class BofreqResponsibleDocEvent(DocEvent): - """ Capture the responsible leadership (IAB and IESG members) for a BOF Request """ - responsible = models.ManyToManyField('person.Person', blank=True) + """Capture the responsible leadership (IAB and IESG members) for a BOF Request""" + + responsible = models.ManyToManyField("person.Person", blank=True) + class StoredObject(models.Model): """Hold metadata about objects placed in object storage""" store = models.CharField(max_length=256) - name = models.CharField(max_length=1024, null=False, blank=False) # N.B. the 1024 limit on name comes from S3 + name = models.CharField( + max_length=1024, null=False, blank=False + ) # N.B. the 1024 limit on name comes from S3 sha384 = models.CharField(max_length=96) len = models.PositiveBigIntegerField() - store_created = models.DateTimeField(help_text="The instant the object ws first placed in the store") + store_created = models.DateTimeField( + help_text="The instant the object ws first placed in the store" + ) created = models.DateTimeField( null=False, - help_text="Instant object became known. May not be the same as the storage's created value for the instance. It will hold ctime for objects imported from older disk storage" + help_text="Instant object became known. May not be the same as the storage's created value for the instance. It will hold ctime for objects imported from older disk storage", ) modified = models.DateTimeField( null=False, - help_text="Last instant object was modified. May not be the same as the storage's modified value for the instance. It will hold mtime for objects imported from older disk storage unless they've actually been overwritten more recently" + help_text="Last instant object was modified. May not be the same as the storage's modified value for the instance. It will hold mtime for objects imported from older disk storage unless they've actually been overwritten more recently", ) doc_name = models.CharField(max_length=255, null=True, blank=True) doc_rev = models.CharField(max_length=16, null=True, blank=True) @@ -1610,7 +1989,9 @@ class StoredObject(models.Model): class Meta: constraints = [ - models.UniqueConstraint(fields=['store', 'name'], name='unique_name_per_store'), + models.UniqueConstraint( + fields=["store", "name"], name="unique_name_per_store" + ), ] indexes = [ models.Index(fields=["doc_name", "doc_rev"]), diff --git a/ietf/doc/redirect_drafts_urls.py b/ietf/doc/redirect_drafts_urls.py index 9673caa26e..5ee473ca41 100644 --- a/ietf/doc/redirect_drafts_urls.py +++ b/ietf/doc/redirect_drafts_urls.py @@ -9,14 +9,33 @@ from ietf.utils.urls import url urlpatterns = [ - url(r'^$', RedirectView.as_view(url='/doc/', permanent=True)), - url(r'^all/$', RedirectView.as_view(url='/doc/all/', permanent=True)), - url(r'^rfc/$', RedirectView.as_view(url='/doc/all/#rfc', permanent=True)), - url(r'^dead/$', RedirectView.as_view(url='/doc/all/#expired', permanent=True)), - url(r'^current/$', RedirectView.as_view(url='/doc/active/', permanent=True)), - url(r'^(?P\d+)/(related/)?$', RedirectView.as_view(url='/doc/', permanent=True)), - url(r'^(?P[^/]+)/(related/)?$', RedirectView.as_view(url='/doc/%(name)s/', permanent=True)), - url(r'^wgid/(?P\d+)/$', lambda request, id: HttpResponsePermanentRedirect("/wg/%s/" % get_object_or_404(Group, id=id).acronym)), - url(r'^wg/(?P[^/]+)/$', RedirectView.as_view(url='/wg/%(acronym)s/', permanent=True)), - url(r'^all_id(?:_txt)?.html$', RedirectView.as_view(url='%s/all_id.txt'%settings.IETF_ID_ARCHIVE_URL, permanent=True)), + url(r"^$", RedirectView.as_view(url="/doc/", permanent=True)), + url(r"^all/$", RedirectView.as_view(url="/doc/all/", permanent=True)), + url(r"^rfc/$", RedirectView.as_view(url="/doc/all/#rfc", permanent=True)), + url(r"^dead/$", RedirectView.as_view(url="/doc/all/#expired", permanent=True)), + url(r"^current/$", RedirectView.as_view(url="/doc/active/", permanent=True)), + url( + r"^(?P\d+)/(related/)?$", + RedirectView.as_view(url="/doc/", permanent=True), + ), + url( + r"^(?P[^/]+)/(related/)?$", + RedirectView.as_view(url="/doc/%(name)s/", permanent=True), + ), + url( + r"^wgid/(?P\d+)/$", + lambda request, id: HttpResponsePermanentRedirect( + "/wg/%s/" % get_object_or_404(Group, id=id).acronym + ), + ), + url( + r"^wg/(?P[^/]+)/$", + RedirectView.as_view(url="/wg/%(acronym)s/", permanent=True), + ), + url( + r"^all_id(?:_txt)?.html$", + RedirectView.as_view( + url="%s/all_id.txt" % settings.IETF_ID_ARCHIVE_URL, permanent=True + ), + ), ] diff --git a/ietf/doc/redirect_idtracker_urls.py b/ietf/doc/redirect_idtracker_urls.py index 8feefbc64f..e0ab2bfb82 100644 --- a/ietf/doc/redirect_idtracker_urls.py +++ b/ietf/doc/redirect_idtracker_urls.py @@ -3,12 +3,32 @@ from ietf.utils.urls import url urlpatterns = [ - url(r'^help/(?:sub)?state/(?:\d+/)?$', RedirectView.as_view(url='/doc/help/state/draft-iesg/', permanent=True)), - url(r'^help/evaluation/$', RedirectView.as_view(url='https://www.ietf.org/iesg/voting-procedures.html', permanent=True)), - url(r'^status/$', RedirectView.as_view(url='/doc/iesg/', permanent=True)), - url(r'^status/last-call/$', RedirectView.as_view(url='/doc/iesg/last-call/', permanent=True)), - url(r'^rfc0*(?P\d+)/$', RedirectView.as_view(url='/doc/rfc%(rfc_number)s/', permanent=True)), - url(r'^(?P[^/]+)/$', RedirectView.as_view(url='/doc/%(name)s/', permanent=True)), - url(r'^(?P[^/]+)/comment/\d+/$', RedirectView.as_view(url='/doc/%(name)s/history/', permanent=True)), - url(r'^$', RedirectView.as_view(url='/doc/', permanent=True)), + url( + r"^help/(?:sub)?state/(?:\d+/)?$", + RedirectView.as_view(url="/doc/help/state/draft-iesg/", permanent=True), + ), + url( + r"^help/evaluation/$", + RedirectView.as_view( + url="https://www.ietf.org/iesg/voting-procedures.html", permanent=True + ), + ), + url(r"^status/$", RedirectView.as_view(url="/doc/iesg/", permanent=True)), + url( + r"^status/last-call/$", + RedirectView.as_view(url="/doc/iesg/last-call/", permanent=True), + ), + url( + r"^rfc0*(?P\d+)/$", + RedirectView.as_view(url="/doc/rfc%(rfc_number)s/", permanent=True), + ), + url( + r"^(?P[^/]+)/$", + RedirectView.as_view(url="/doc/%(name)s/", permanent=True), + ), + url( + r"^(?P[^/]+)/comment/\d+/$", + RedirectView.as_view(url="/doc/%(name)s/history/", permanent=True), + ), + url(r"^$", RedirectView.as_view(url="/doc/", permanent=True)), ] diff --git a/ietf/doc/resources.py b/ietf/doc/resources.py index 157a3ad556..0b42194915 100644 --- a/ietf/doc/resources.py +++ b/ietf/doc/resources.py @@ -6,31 +6,64 @@ from ietf.api import ModelResource from ietf.api import ToOneField from tastypie.fields import ToManyField, CharField -from tastypie.constants import ALL, ALL_WITH_RELATIONS # pyflakes:ignore +from tastypie.constants import ALL, ALL_WITH_RELATIONS # pyflakes:ignore from tastypie.cache import SimpleCache from ietf import api -from ietf.doc.models import (BallotType, DeletedEvent, StateType, State, Document, - DocumentAuthor, DocEvent, StateDocEvent, DocHistory, ConsensusDocEvent, - TelechatDocEvent, DocReminder, LastCallDocEvent, NewRevisionDocEvent, WriteupDocEvent, - InitialReviewDocEvent, DocHistoryAuthor, BallotDocEvent, RelatedDocument, - RelatedDocHistory, BallotPositionDocEvent, AddedMessageEvent, SubmissionDocEvent, - ReviewRequestDocEvent, ReviewAssignmentDocEvent, EditedAuthorsDocEvent, DocumentURL, - IanaExpertDocEvent, IRSGBallotDocEvent, DocExtResource, DocumentActionHolder, - BofreqEditorDocEvent, BofreqResponsibleDocEvent, StoredObject) +from ietf.doc.models import ( + BallotType, + DeletedEvent, + StateType, + State, + Document, + DocumentAuthor, + DocEvent, + StateDocEvent, + DocHistory, + ConsensusDocEvent, + TelechatDocEvent, + DocReminder, + LastCallDocEvent, + NewRevisionDocEvent, + WriteupDocEvent, + InitialReviewDocEvent, + DocHistoryAuthor, + BallotDocEvent, + RelatedDocument, + RelatedDocHistory, + BallotPositionDocEvent, + AddedMessageEvent, + SubmissionDocEvent, + ReviewRequestDocEvent, + ReviewAssignmentDocEvent, + EditedAuthorsDocEvent, + DocumentURL, + IanaExpertDocEvent, + IRSGBallotDocEvent, + DocExtResource, + DocumentActionHolder, + BofreqEditorDocEvent, + BofreqResponsibleDocEvent, + StoredObject, +) from ietf.name.resources import BallotPositionNameResource, DocTypeNameResource + + class BallotTypeResource(ModelResource): - doc_type = ToOneField(DocTypeNameResource, 'doc_type', null=True) - positions = ToManyField(BallotPositionNameResource, 'positions', null=True) + doc_type = ToOneField(DocTypeNameResource, "doc_type", null=True) + positions = ToManyField(BallotPositionNameResource, "positions", null=True) + class Meta: cache = SimpleCache() queryset = BallotType.objects.all() serializer = api.Serializer() - #resource_name = 'ballottype' - ordering = ['id', ] - filtering = { + # resource_name = 'ballottype' + ordering = [ + "id", + ] + filtering = { "id": ALL, "slug": ALL, "name": ALL, @@ -40,51 +73,71 @@ class Meta: "doc_type": ALL_WITH_RELATIONS, "positions": ALL_WITH_RELATIONS, } + + api.doc.register(BallotTypeResource()) from ietf.person.resources import PersonResource from ietf.utils.resources import ContentTypeResource + + class DeletedEventResource(ModelResource): - content_type = ToOneField(ContentTypeResource, 'content_type') - by = ToOneField(PersonResource, 'by') + content_type = ToOneField(ContentTypeResource, "content_type") + by = ToOneField(PersonResource, "by") + class Meta: cache = SimpleCache() queryset = DeletedEvent.objects.all() serializer = api.Serializer() - #resource_name = 'deletedevent' - ordering = ['id', ] - filtering = { + # resource_name = 'deletedevent' + ordering = [ + "id", + ] + filtering = { "id": ALL, "json": ALL, "time": ALL, "content_type": ALL_WITH_RELATIONS, "by": ALL_WITH_RELATIONS, } + + api.doc.register(DeletedEventResource()) + class StateTypeResource(ModelResource): class Meta: cache = SimpleCache() queryset = StateType.objects.all() serializer = api.Serializer() - #resource_name = 'statetype' - ordering = ['id', ] - filtering = { + # resource_name = 'statetype' + ordering = [ + "id", + ] + filtering = { "slug": ALL, "label": ALL, } + + api.doc.register(StateTypeResource()) + class StateResource(ModelResource): - type = ToOneField(StateTypeResource, 'type') - next_states = ToManyField('ietf.doc.resources.StateResource', 'next_states', null=True) + type = ToOneField(StateTypeResource, "type") + next_states = ToManyField( + "ietf.doc.resources.StateResource", "next_states", null=True + ) + class Meta: cache = SimpleCache() queryset = State.objects.all() serializer = api.Serializer() - #resource_name = 'state' - ordering = ['id', ] - filtering = { + # resource_name = 'state' + ordering = [ + "id", + ] + filtering = { "id": ALL, "slug": ALL, "name": ALL, @@ -94,31 +147,48 @@ class Meta: "type": ALL_WITH_RELATIONS, "next_states": ALL_WITH_RELATIONS, } + + api.doc.register(StateResource()) from ietf.person.resources import PersonResource, EmailResource from ietf.group.resources import GroupResource -from ietf.name.resources import StdLevelNameResource, StreamNameResource, DocTypeNameResource, DocTagNameResource, IntendedStdLevelNameResource +from ietf.name.resources import ( + StdLevelNameResource, + StreamNameResource, + DocTypeNameResource, + DocTagNameResource, + IntendedStdLevelNameResource, +) + + class DocumentResource(ModelResource): - type = ToOneField(DocTypeNameResource, 'type', null=True) - stream = ToOneField(StreamNameResource, 'stream', null=True) - group = ToOneField(GroupResource, 'group', null=True) - intended_std_level = ToOneField(IntendedStdLevelNameResource, 'intended_std_level', null=True) - std_level = ToOneField(StdLevelNameResource, 'std_level', null=True) - ad = ToOneField(PersonResource, 'ad', null=True) - shepherd = ToOneField(EmailResource, 'shepherd', null=True) - states = ToManyField(StateResource, 'states', null=True) - tags = ToManyField(DocTagNameResource, 'tags', null=True) - rfc = CharField(attribute='rfc_number', null=True) - submissions = ToManyField('ietf.submit.resources.SubmissionResource', 'submission_set', null=True) + type = ToOneField(DocTypeNameResource, "type", null=True) + stream = ToOneField(StreamNameResource, "stream", null=True) + group = ToOneField(GroupResource, "group", null=True) + intended_std_level = ToOneField( + IntendedStdLevelNameResource, "intended_std_level", null=True + ) + std_level = ToOneField(StdLevelNameResource, "std_level", null=True) + ad = ToOneField(PersonResource, "ad", null=True) + shepherd = ToOneField(EmailResource, "shepherd", null=True) + states = ToManyField(StateResource, "states", null=True) + tags = ToManyField(DocTagNameResource, "tags", null=True) + rfc = CharField(attribute="rfc_number", null=True) + submissions = ToManyField( + "ietf.submit.resources.SubmissionResource", "submission_set", null=True + ) + class Meta: cache = SimpleCache() queryset = Document.objects.all() serializer = api.Serializer() - detail_uri_name = 'name' - #resource_name = 'document' - ordering = ['id', ] - filtering = { + detail_uri_name = "name" + # resource_name = 'document' + ordering = [ + "id", + ] + filtering = { "time": ALL, "title": ALL, "abstract": ALL, @@ -141,20 +211,27 @@ class Meta: "states": ALL_WITH_RELATIONS, "tags": ALL_WITH_RELATIONS, } + + api.doc.register(DocumentResource()) from ietf.person.resources import PersonResource, EmailResource + + class DocumentAuthorResource(ModelResource): - person = ToOneField(PersonResource, 'person') - email = ToOneField(EmailResource, 'email', null=True) - document = ToOneField(DocumentResource, 'document') + person = ToOneField(PersonResource, "person") + email = ToOneField(EmailResource, "email", null=True) + document = ToOneField(DocumentResource, "document") + class Meta: cache = SimpleCache() queryset = DocumentAuthor.objects.all() serializer = api.Serializer() - #resource_name = 'documentauthor' - ordering = ['id', ] - filtering = { + # resource_name = 'documentauthor' + ordering = [ + "id", + ] + filtering = { "id": ALL, "affiliation": ALL, "country": ALL, @@ -163,19 +240,26 @@ class Meta: "email": ALL_WITH_RELATIONS, "document": ALL_WITH_RELATIONS, } + + api.doc.register(DocumentAuthorResource()) from ietf.person.resources import PersonResource + + class DocEventResource(ModelResource): - by = ToOneField(PersonResource, 'by') - doc = ToOneField(DocumentResource, 'doc') + by = ToOneField(PersonResource, "by") + doc = ToOneField(DocumentResource, "doc") + class Meta: cache = SimpleCache() queryset = DocEvent.objects.all() serializer = api.Serializer() - #resource_name = 'docevent' - ordering = ['id', ] - filtering = { + # resource_name = 'docevent' + ordering = [ + "id", + ] + filtering = { "id": ALL, "time": ALL, "type": ALL, @@ -184,22 +268,29 @@ class Meta: "by": ALL_WITH_RELATIONS, "doc": ALL_WITH_RELATIONS, } + + api.doc.register(DocEventResource()) from ietf.person.resources import PersonResource + + class StateDocEventResource(ModelResource): - by = ToOneField(PersonResource, 'by') - doc = ToOneField(DocumentResource, 'doc') - docevent_ptr = ToOneField(DocEventResource, 'docevent_ptr') - state_type = ToOneField(StateTypeResource, 'state_type') - state = ToOneField(StateResource, 'state', null=True) + by = ToOneField(PersonResource, "by") + doc = ToOneField(DocumentResource, "doc") + docevent_ptr = ToOneField(DocEventResource, "docevent_ptr") + state_type = ToOneField(StateTypeResource, "state_type") + state = ToOneField(StateResource, "state", null=True) + class Meta: cache = SimpleCache() queryset = StateDocEvent.objects.all() serializer = api.Serializer() - #resource_name = 'statedocevent' - ordering = ['id', ] - filtering = { + # resource_name = 'statedocevent' + ordering = [ + "id", + ] + filtering = { "id": ALL, "time": ALL, "type": ALL, @@ -211,29 +302,44 @@ class Meta: "state_type": ALL_WITH_RELATIONS, "state": ALL_WITH_RELATIONS, } + + api.doc.register(StateDocEventResource()) from ietf.person.resources import PersonResource, EmailResource from ietf.group.resources import GroupResource -from ietf.name.resources import StdLevelNameResource, StreamNameResource, DocTypeNameResource, DocTagNameResource, IntendedStdLevelNameResource +from ietf.name.resources import ( + StdLevelNameResource, + StreamNameResource, + DocTypeNameResource, + DocTagNameResource, + IntendedStdLevelNameResource, +) + + class DocHistoryResource(ModelResource): - type = ToOneField(DocTypeNameResource, 'type', null=True) - stream = ToOneField(StreamNameResource, 'stream', null=True) - group = ToOneField(GroupResource, 'group', null=True) - intended_std_level = ToOneField(IntendedStdLevelNameResource, 'intended_std_level', null=True) - std_level = ToOneField(StdLevelNameResource, 'std_level', null=True) - ad = ToOneField(PersonResource, 'ad', null=True) - shepherd = ToOneField(EmailResource, 'shepherd', null=True) - doc = ToOneField(DocumentResource, 'doc') - states = ToManyField(StateResource, 'states', null=True) - tags = ToManyField(DocTagNameResource, 'tags', null=True) + type = ToOneField(DocTypeNameResource, "type", null=True) + stream = ToOneField(StreamNameResource, "stream", null=True) + group = ToOneField(GroupResource, "group", null=True) + intended_std_level = ToOneField( + IntendedStdLevelNameResource, "intended_std_level", null=True + ) + std_level = ToOneField(StdLevelNameResource, "std_level", null=True) + ad = ToOneField(PersonResource, "ad", null=True) + shepherd = ToOneField(EmailResource, "shepherd", null=True) + doc = ToOneField(DocumentResource, "doc") + states = ToManyField(StateResource, "states", null=True) + tags = ToManyField(DocTagNameResource, "tags", null=True) + class Meta: cache = SimpleCache() queryset = DocHistory.objects.all() serializer = api.Serializer() - #resource_name = 'dochistory' - ordering = ['id', ] - filtering = { + # resource_name = 'dochistory' + ordering = [ + "id", + ] + filtering = { "id": ALL, "time": ALL, "title": ALL, @@ -258,20 +364,27 @@ class Meta: "states": ALL_WITH_RELATIONS, "tags": ALL_WITH_RELATIONS, } + + api.doc.register(DocHistoryResource()) from ietf.person.resources import PersonResource + + class ConsensusDocEventResource(ModelResource): - by = ToOneField(PersonResource, 'by') - doc = ToOneField(DocumentResource, 'doc') - docevent_ptr = ToOneField(DocEventResource, 'docevent_ptr') + by = ToOneField(PersonResource, "by") + doc = ToOneField(DocumentResource, "doc") + docevent_ptr = ToOneField(DocEventResource, "docevent_ptr") + class Meta: cache = SimpleCache() queryset = ConsensusDocEvent.objects.all() serializer = api.Serializer() - #resource_name = 'consensusdocevent' - ordering = ['id', ] - filtering = { + # resource_name = 'consensusdocevent' + ordering = [ + "id", + ] + filtering = { "id": ALL, "time": ALL, "type": ALL, @@ -282,20 +395,27 @@ class Meta: "doc": ALL_WITH_RELATIONS, "docevent_ptr": ALL_WITH_RELATIONS, } + + api.doc.register(ConsensusDocEventResource()) from ietf.person.resources import PersonResource + + class TelechatDocEventResource(ModelResource): - by = ToOneField(PersonResource, 'by') - doc = ToOneField(DocumentResource, 'doc') - docevent_ptr = ToOneField(DocEventResource, 'docevent_ptr') + by = ToOneField(PersonResource, "by") + doc = ToOneField(DocumentResource, "doc") + docevent_ptr = ToOneField(DocEventResource, "docevent_ptr") + class Meta: cache = SimpleCache() queryset = TelechatDocEvent.objects.all() serializer = api.Serializer() - #resource_name = 'telechatdocevent' - ordering = ['id', ] - filtering = { + # resource_name = 'telechatdocevent' + ordering = [ + "id", + ] + filtering = { "id": ALL, "time": ALL, "type": ALL, @@ -307,39 +427,53 @@ class Meta: "doc": ALL_WITH_RELATIONS, "docevent_ptr": ALL_WITH_RELATIONS, } + + api.doc.register(TelechatDocEventResource()) from ietf.name.resources import DocReminderTypeNameResource + + class DocReminderResource(ModelResource): - event = ToOneField(DocEventResource, 'event') - type = ToOneField(DocReminderTypeNameResource, 'type') + event = ToOneField(DocEventResource, "event") + type = ToOneField(DocReminderTypeNameResource, "type") + class Meta: cache = SimpleCache() queryset = DocReminder.objects.all() serializer = api.Serializer() - #resource_name = 'docreminder' - ordering = ['id', ] - filtering = { + # resource_name = 'docreminder' + ordering = [ + "id", + ] + filtering = { "id": ALL, "due": ALL, "active": ALL, "event": ALL_WITH_RELATIONS, "type": ALL_WITH_RELATIONS, } + + api.doc.register(DocReminderResource()) from ietf.person.resources import PersonResource + + class LastCallDocEventResource(ModelResource): - by = ToOneField(PersonResource, 'by') - doc = ToOneField(DocumentResource, 'doc') - docevent_ptr = ToOneField(DocEventResource, 'docevent_ptr') + by = ToOneField(PersonResource, "by") + doc = ToOneField(DocumentResource, "doc") + docevent_ptr = ToOneField(DocEventResource, "docevent_ptr") + class Meta: cache = SimpleCache() queryset = LastCallDocEvent.objects.all() serializer = api.Serializer() - #resource_name = 'lastcalldocevent' - ordering = ['id', ] - filtering = { + # resource_name = 'lastcalldocevent' + ordering = [ + "id", + ] + filtering = { "id": ALL, "time": ALL, "type": ALL, @@ -350,20 +484,27 @@ class Meta: "doc": ALL_WITH_RELATIONS, "docevent_ptr": ALL_WITH_RELATIONS, } + + api.doc.register(LastCallDocEventResource()) from ietf.person.resources import PersonResource + + class NewRevisionDocEventResource(ModelResource): - by = ToOneField(PersonResource, 'by') - doc = ToOneField(DocumentResource, 'doc') - docevent_ptr = ToOneField(DocEventResource, 'docevent_ptr') + by = ToOneField(PersonResource, "by") + doc = ToOneField(DocumentResource, "doc") + docevent_ptr = ToOneField(DocEventResource, "docevent_ptr") + class Meta: cache = SimpleCache() queryset = NewRevisionDocEvent.objects.all() serializer = api.Serializer() - #resource_name = 'newrevisiondocevent' - ordering = ['id', ] - filtering = { + # resource_name = 'newrevisiondocevent' + ordering = [ + "id", + ] + filtering = { "id": ALL, "time": ALL, "type": ALL, @@ -373,20 +514,27 @@ class Meta: "doc": ALL_WITH_RELATIONS, "docevent_ptr": ALL_WITH_RELATIONS, } + + api.doc.register(NewRevisionDocEventResource()) from ietf.person.resources import PersonResource + + class WriteupDocEventResource(ModelResource): - by = ToOneField(PersonResource, 'by') - doc = ToOneField(DocumentResource, 'doc') - docevent_ptr = ToOneField(DocEventResource, 'docevent_ptr') + by = ToOneField(PersonResource, "by") + doc = ToOneField(DocumentResource, "doc") + docevent_ptr = ToOneField(DocEventResource, "docevent_ptr") + class Meta: cache = SimpleCache() queryset = WriteupDocEvent.objects.all() serializer = api.Serializer() - #resource_name = 'writeupdocevent' - ordering = ['id', ] - filtering = { + # resource_name = 'writeupdocevent' + ordering = [ + "id", + ] + filtering = { "id": ALL, "time": ALL, "type": ALL, @@ -397,20 +545,27 @@ class Meta: "doc": ALL_WITH_RELATIONS, "docevent_ptr": ALL_WITH_RELATIONS, } + + api.doc.register(WriteupDocEventResource()) from ietf.person.resources import PersonResource + + class InitialReviewDocEventResource(ModelResource): - by = ToOneField(PersonResource, 'by') - doc = ToOneField(DocumentResource, 'doc') - docevent_ptr = ToOneField(DocEventResource, 'docevent_ptr') + by = ToOneField(PersonResource, "by") + doc = ToOneField(DocumentResource, "doc") + docevent_ptr = ToOneField(DocEventResource, "docevent_ptr") + class Meta: cache = SimpleCache() queryset = InitialReviewDocEvent.objects.all() serializer = api.Serializer() - #resource_name = 'initialreviewdocevent' - ordering = ['id', ] - filtering = { + # resource_name = 'initialreviewdocevent' + ordering = [ + "id", + ] + filtering = { "id": ALL, "time": ALL, "type": ALL, @@ -421,20 +576,27 @@ class Meta: "doc": ALL_WITH_RELATIONS, "docevent_ptr": ALL_WITH_RELATIONS, } + + api.doc.register(InitialReviewDocEventResource()) from ietf.person.resources import PersonResource, EmailResource + + class DocHistoryAuthorResource(ModelResource): - person = ToOneField(PersonResource, 'person') - email = ToOneField(EmailResource, 'email', null=True) - document = ToOneField(DocHistoryResource, 'document') + person = ToOneField(PersonResource, "person") + email = ToOneField(EmailResource, "email", null=True) + document = ToOneField(DocHistoryResource, "document") + class Meta: cache = SimpleCache() queryset = DocHistoryAuthor.objects.all() serializer = api.Serializer() - #resource_name = 'dochistoryauthor' - ordering = ['id', ] - filtering = { + # resource_name = 'dochistoryauthor' + ordering = [ + "id", + ] + filtering = { "id": ALL, "affiliation": ALL, "country": ALL, @@ -443,21 +605,28 @@ class Meta: "email": ALL_WITH_RELATIONS, "document": ALL_WITH_RELATIONS, } + + api.doc.register(DocHistoryAuthorResource()) from ietf.person.resources import PersonResource + + class BallotDocEventResource(ModelResource): - by = ToOneField(PersonResource, 'by') - doc = ToOneField(DocumentResource, 'doc') - docevent_ptr = ToOneField(DocEventResource, 'docevent_ptr') - ballot_type = ToOneField(BallotTypeResource, 'ballot_type') + by = ToOneField(PersonResource, "by") + doc = ToOneField(DocumentResource, "doc") + docevent_ptr = ToOneField(DocEventResource, "docevent_ptr") + ballot_type = ToOneField(BallotTypeResource, "ballot_type") + class Meta: cache = SimpleCache() queryset = BallotDocEvent.objects.all() serializer = api.Serializer() - #resource_name = 'ballotdocevent' - ordering = ['id', ] - filtering = { + # resource_name = 'ballotdocevent' + ordering = [ + "id", + ] + filtering = { "id": ALL, "time": ALL, "type": ALL, @@ -468,62 +637,83 @@ class Meta: "docevent_ptr": ALL_WITH_RELATIONS, "ballot_type": ALL_WITH_RELATIONS, } + + api.doc.register(BallotDocEventResource()) from ietf.name.resources import DocRelationshipNameResource + + class RelatedDocumentResource(ModelResource): - source = ToOneField(DocumentResource, 'source') - target = ToOneField(DocumentResource, 'target') - relationship = ToOneField(DocRelationshipNameResource, 'relationship') + source = ToOneField(DocumentResource, "source") + target = ToOneField(DocumentResource, "target") + relationship = ToOneField(DocRelationshipNameResource, "relationship") + class Meta: cache = SimpleCache() queryset = RelatedDocument.objects.all() serializer = api.Serializer() - #resource_name = 'relateddocument' - ordering = ['id', ] - filtering = { + # resource_name = 'relateddocument' + ordering = [ + "id", + ] + filtering = { "id": ALL, "source": ALL_WITH_RELATIONS, "target": ALL_WITH_RELATIONS, "relationship": ALL_WITH_RELATIONS, } + + api.doc.register(RelatedDocumentResource()) from ietf.name.resources import DocRelationshipNameResource + + class RelatedDocHistoryResource(ModelResource): - source = ToOneField(DocHistoryResource, 'source') - target = ToOneField(DocumentResource, 'target') - relationship = ToOneField(DocRelationshipNameResource, 'relationship') + source = ToOneField(DocHistoryResource, "source") + target = ToOneField(DocumentResource, "target") + relationship = ToOneField(DocRelationshipNameResource, "relationship") + class Meta: cache = SimpleCache() queryset = RelatedDocHistory.objects.all() serializer = api.Serializer() - #resource_name = 'relateddochistory' - ordering = ['id', ] - filtering = { + # resource_name = 'relateddochistory' + ordering = [ + "id", + ] + filtering = { "id": ALL, "source": ALL_WITH_RELATIONS, "target": ALL_WITH_RELATIONS, "relationship": ALL_WITH_RELATIONS, } + + api.doc.register(RelatedDocHistoryResource()) from ietf.person.resources import PersonResource from ietf.name.resources import BallotPositionNameResource + + class BallotPositionDocEventResource(ModelResource): - by = ToOneField(PersonResource, 'by') - doc = ToOneField(DocumentResource, 'doc') - docevent_ptr = ToOneField(DocEventResource, 'docevent_ptr') - ballot = ToOneField(BallotDocEventResource, 'ballot', null=True) - balloter = ToOneField(PersonResource, 'balloter') - pos = ToOneField(BallotPositionNameResource, 'pos') + by = ToOneField(PersonResource, "by") + doc = ToOneField(DocumentResource, "doc") + docevent_ptr = ToOneField(DocEventResource, "docevent_ptr") + ballot = ToOneField(BallotDocEventResource, "ballot", null=True) + balloter = ToOneField(PersonResource, "balloter") + pos = ToOneField(BallotPositionNameResource, "pos") + class Meta: cache = SimpleCache() queryset = BallotPositionDocEvent.objects.all() serializer = api.Serializer() - #resource_name = 'ballotpositiondocevent' - ordering = ['id', ] - filtering = { + # resource_name = 'ballotpositiondocevent' + ordering = [ + "id", + ] + filtering = { "id": ALL, "time": ALL, "type": ALL, @@ -540,23 +730,30 @@ class Meta: "balloter": ALL_WITH_RELATIONS, "pos": ALL_WITH_RELATIONS, } + + api.doc.register(BallotPositionDocEventResource()) from ietf.person.resources import PersonResource from ietf.message.resources import MessageResource + + class AddedMessageEventResource(ModelResource): - by = ToOneField(PersonResource, 'by') - doc = ToOneField(DocumentResource, 'doc') - docevent_ptr = ToOneField(DocEventResource, 'docevent_ptr') - message = ToOneField(MessageResource, 'message', null=True) - in_reply_to = ToOneField(MessageResource, 'in_reply_to', null=True) + by = ToOneField(PersonResource, "by") + doc = ToOneField(DocumentResource, "doc") + docevent_ptr = ToOneField(DocEventResource, "docevent_ptr") + message = ToOneField(MessageResource, "message", null=True) + in_reply_to = ToOneField(MessageResource, "in_reply_to", null=True) + class Meta: queryset = AddedMessageEvent.objects.all() serializer = api.Serializer() cache = SimpleCache() - #resource_name = 'addedmessageevent' - ordering = ['id', ] - filtering = { + # resource_name = 'addedmessageevent' + ordering = [ + "id", + ] + filtering = { "id": ALL, "time": ALL, "type": ALL, @@ -569,22 +766,29 @@ class Meta: "message": ALL_WITH_RELATIONS, "in_reply_to": ALL_WITH_RELATIONS, } + + api.doc.register(AddedMessageEventResource()) from ietf.person.resources import PersonResource from ietf.submit.resources import SubmissionResource + + class SubmissionDocEventResource(ModelResource): - by = ToOneField(PersonResource, 'by') - doc = ToOneField(DocumentResource, 'doc') - docevent_ptr = ToOneField(DocEventResource, 'docevent_ptr') - submission = ToOneField(SubmissionResource, 'submission') + by = ToOneField(PersonResource, "by") + doc = ToOneField(DocumentResource, "doc") + docevent_ptr = ToOneField(DocEventResource, "docevent_ptr") + submission = ToOneField(SubmissionResource, "submission") + class Meta: queryset = SubmissionDocEvent.objects.all() serializer = api.Serializer() cache = SimpleCache() - #resource_name = 'submissiondocevent' - ordering = ['id', ] - filtering = { + # resource_name = 'submissiondocevent' + ordering = [ + "id", + ] + filtering = { "id": ALL, "time": ALL, "type": ALL, @@ -595,23 +799,32 @@ class Meta: "docevent_ptr": ALL_WITH_RELATIONS, "submission": ALL_WITH_RELATIONS, } + + api.doc.register(SubmissionDocEventResource()) from ietf.person.resources import PersonResource from ietf.name.resources import ReviewRequestStateNameResource + + class ReviewRequestDocEventResource(ModelResource): - by = ToOneField(PersonResource, 'by') - doc = ToOneField(DocumentResource, 'doc') - docevent_ptr = ToOneField(DocEventResource, 'docevent_ptr') - review_request = ToOneField('ietf.review.resources.ReviewRequestResource', 'review_request') - state = ToOneField(ReviewRequestStateNameResource, 'state', null=True) + by = ToOneField(PersonResource, "by") + doc = ToOneField(DocumentResource, "doc") + docevent_ptr = ToOneField(DocEventResource, "docevent_ptr") + review_request = ToOneField( + "ietf.review.resources.ReviewRequestResource", "review_request" + ) + state = ToOneField(ReviewRequestStateNameResource, "state", null=True) + class Meta: queryset = ReviewRequestDocEvent.objects.all() serializer = api.Serializer() cache = SimpleCache() - #resource_name = 'reviewrequestdocevent' - ordering = ['id', ] - filtering = { + # resource_name = 'reviewrequestdocevent' + ordering = [ + "id", + ] + filtering = { "id": ALL, "time": ALL, "type": ALL, @@ -623,20 +836,27 @@ class Meta: "review_request": ALL_WITH_RELATIONS, "state": ALL_WITH_RELATIONS, } + + api.doc.register(ReviewRequestDocEventResource()) from ietf.person.resources import PersonResource + + class EditedAuthorsDocEventResource(ModelResource): - by = ToOneField(PersonResource, 'by') - doc = ToOneField(DocumentResource, 'doc') - docevent_ptr = ToOneField(DocEventResource, 'docevent_ptr') + by = ToOneField(PersonResource, "by") + doc = ToOneField(DocumentResource, "doc") + docevent_ptr = ToOneField(DocEventResource, "docevent_ptr") + class Meta: queryset = EditedAuthorsDocEvent.objects.all() serializer = api.Serializer() cache = SimpleCache() - #resource_name = 'editedauthorsdocevent' - ordering = ['id', ] - filtering = { + # resource_name = 'editedauthorsdocevent' + ordering = [ + "id", + ] + filtering = { "id": ALL, "time": ALL, "type": ALL, @@ -647,45 +867,59 @@ class Meta: "doc": ALL_WITH_RELATIONS, "docevent_ptr": ALL_WITH_RELATIONS, } + + api.doc.register(EditedAuthorsDocEventResource()) from ietf.name.resources import DocUrlTagNameResource + + class DocumentURLResource(ModelResource): - doc = ToOneField(DocumentResource, 'doc') - tag = ToOneField(DocUrlTagNameResource, 'tag') + doc = ToOneField(DocumentResource, "doc") + tag = ToOneField(DocUrlTagNameResource, "tag") + class Meta: queryset = DocumentURL.objects.all() serializer = api.Serializer() cache = SimpleCache() - #resource_name = 'documenturl' - ordering = ['id', ] - filtering = { + # resource_name = 'documenturl' + ordering = [ + "id", + ] + filtering = { "id": ALL, "desc": ALL, "url": ALL, "doc": ALL_WITH_RELATIONS, "tag": ALL_WITH_RELATIONS, } + + api.doc.register(DocumentURLResource()) from ietf.person.resources import PersonResource from ietf.review.resources import ReviewAssignmentResource from ietf.name.resources import ReviewAssignmentStateNameResource + + class ReviewAssignmentDocEventResource(ModelResource): - by = ToOneField(PersonResource, 'by') - doc = ToOneField(DocumentResource, 'doc') - docevent_ptr = ToOneField(DocEventResource, 'docevent_ptr') - review_assignment = ToOneField(ReviewAssignmentResource, 'review_assignment') - state = ToOneField(ReviewAssignmentStateNameResource, 'state', null=True) + by = ToOneField(PersonResource, "by") + doc = ToOneField(DocumentResource, "doc") + docevent_ptr = ToOneField(DocEventResource, "docevent_ptr") + review_assignment = ToOneField(ReviewAssignmentResource, "review_assignment") + state = ToOneField(ReviewAssignmentStateNameResource, "state", null=True) + class Meta: queryset = ReviewAssignmentDocEvent.objects.all() serializer = api.Serializer() cache = SimpleCache() - #resource_name = 'reviewassignmentdocevent' - ordering = ['id', ] - filtering = { + # resource_name = 'reviewassignmentdocevent' + ordering = [ + "id", + ] + filtering = { "id": ALL, "time": ALL, "type": ALL, @@ -697,21 +931,28 @@ class Meta: "review_assignment": ALL_WITH_RELATIONS, "state": ALL_WITH_RELATIONS, } + + api.doc.register(ReviewAssignmentDocEventResource()) from ietf.person.resources import PersonResource + + class IanaExpertDocEventResource(ModelResource): - by = ToOneField(PersonResource, 'by') - doc = ToOneField(DocumentResource, 'doc') - docevent_ptr = ToOneField(DocEventResource, 'docevent_ptr') + by = ToOneField(PersonResource, "by") + doc = ToOneField(DocumentResource, "doc") + docevent_ptr = ToOneField(DocEventResource, "docevent_ptr") + class Meta: queryset = IanaExpertDocEvent.objects.all() serializer = api.Serializer() cache = SimpleCache() - #resource_name = 'ianaexpertdocevent' - ordering = ['docevent_ptr', ] - filtering = { + # resource_name = 'ianaexpertdocevent' + ordering = [ + "docevent_ptr", + ] + filtering = { "id": ALL, "time": ALL, "type": ALL, @@ -721,23 +962,30 @@ class Meta: "doc": ALL_WITH_RELATIONS, "docevent_ptr": ALL_WITH_RELATIONS, } + + api.doc.register(IanaExpertDocEventResource()) from ietf.person.resources import PersonResource + + class IRSGBallotDocEventResource(ModelResource): - by = ToOneField(PersonResource, 'by') - doc = ToOneField(DocumentResource, 'doc') - docevent_ptr = ToOneField(DocEventResource, 'docevent_ptr') - ballot_type = ToOneField(BallotTypeResource, 'ballot_type') - ballotdocevent_ptr = ToOneField(BallotDocEventResource, 'ballotdocevent_ptr') + by = ToOneField(PersonResource, "by") + doc = ToOneField(DocumentResource, "doc") + docevent_ptr = ToOneField(DocEventResource, "docevent_ptr") + ballot_type = ToOneField(BallotTypeResource, "ballot_type") + ballotdocevent_ptr = ToOneField(BallotDocEventResource, "ballotdocevent_ptr") + class Meta: queryset = IRSGBallotDocEvent.objects.all() serializer = api.Serializer() cache = SimpleCache() - #resource_name = 'irsgballotdocevent' - ordering = ['ballotdocevent_ptr', ] - filtering = { + # resource_name = 'irsgballotdocevent' + ordering = [ + "ballotdocevent_ptr", + ] + filtering = { "id": ALL, "time": ALL, "type": ALL, @@ -750,61 +998,82 @@ class Meta: "ballot_type": ALL_WITH_RELATIONS, "ballotdocevent_ptr": ALL_WITH_RELATIONS, } + + api.doc.register(IRSGBallotDocEventResource()) from ietf.name.resources import ExtResourceNameResource + + class DocExtResourceResource(ModelResource): - doc = ToOneField(DocumentResource, 'doc') - name = ToOneField(ExtResourceNameResource, 'name') + doc = ToOneField(DocumentResource, "doc") + name = ToOneField(ExtResourceNameResource, "name") + class Meta: queryset = DocExtResource.objects.all() serializer = api.Serializer() cache = SimpleCache() - resource_name = 'docextresource' - ordering = ['id', ] - filtering = { + resource_name = "docextresource" + ordering = [ + "id", + ] + filtering = { "id": ALL, "display_name": ALL, "value": ALL, "doc": ALL_WITH_RELATIONS, "name": ALL_WITH_RELATIONS, } + + api.doc.register(DocExtResourceResource()) from ietf.person.resources import PersonResource + + class DocumentActionHolderResource(ModelResource): - document = ToOneField(DocumentResource, 'document') - person = ToOneField(PersonResource, 'person') + document = ToOneField(DocumentResource, "document") + person = ToOneField(PersonResource, "person") + class Meta: queryset = DocumentActionHolder.objects.all() serializer = api.Serializer() cache = SimpleCache() - #resource_name = 'documentactionholder' - ordering = ['id', ] - filtering = { + # resource_name = 'documentactionholder' + ordering = [ + "id", + ] + filtering = { "id": ALL, "time_added": ALL, "document": ALL_WITH_RELATIONS, "person": ALL_WITH_RELATIONS, } + + api.doc.register(DocumentActionHolderResource()) from ietf.person.resources import PersonResource + + class BofreqEditorDocEventResource(ModelResource): - by = ToOneField(PersonResource, 'by') - doc = ToOneField(DocumentResource, 'doc') - docevent_ptr = ToOneField(DocEventResource, 'docevent_ptr') - editors = ToManyField(PersonResource, 'editors', null=True) + by = ToOneField(PersonResource, "by") + doc = ToOneField(DocumentResource, "doc") + docevent_ptr = ToOneField(DocEventResource, "docevent_ptr") + editors = ToManyField(PersonResource, "editors", null=True) + class Meta: queryset = BofreqEditorDocEvent.objects.all() serializer = api.Serializer() cache = SimpleCache() - #resource_name = 'bofreqeditordocevent' - ordering = ['docevent_ptr', ] - filtering = { + # resource_name = 'bofreqeditordocevent' + ordering = [ + "docevent_ptr", + ] + filtering = { "id": ALL, "time": ALL, "type": ALL, @@ -815,22 +1084,29 @@ class Meta: "docevent_ptr": ALL_WITH_RELATIONS, "editors": ALL_WITH_RELATIONS, } + + api.doc.register(BofreqEditorDocEventResource()) from ietf.person.resources import PersonResource + + class BofreqResponsibleDocEventResource(ModelResource): - by = ToOneField(PersonResource, 'by') - doc = ToOneField(DocumentResource, 'doc') - docevent_ptr = ToOneField(DocEventResource, 'docevent_ptr') - responsible = ToManyField(PersonResource, 'responsible', null=True) + by = ToOneField(PersonResource, "by") + doc = ToOneField(DocumentResource, "doc") + docevent_ptr = ToOneField(DocEventResource, "docevent_ptr") + responsible = ToManyField(PersonResource, "responsible", null=True) + class Meta: queryset = BofreqResponsibleDocEvent.objects.all() serializer = api.Serializer() cache = SimpleCache() - #resource_name = 'bofreqresponsibledocevent' - ordering = ['docevent_ptr', ] - filtering = { + # resource_name = 'bofreqresponsibledocevent' + ordering = [ + "docevent_ptr", + ] + filtering = { "id": ALL, "time": ALL, "type": ALL, @@ -841,6 +1117,8 @@ class Meta: "docevent_ptr": ALL_WITH_RELATIONS, "responsible": ALL_WITH_RELATIONS, } + + api.doc.register(BofreqResponsibleDocEventResource()) @@ -849,9 +1127,11 @@ class Meta: queryset = StoredObject.objects.all() serializer = api.Serializer() cache = SimpleCache() - #resource_name = 'storedobject' - ordering = ['id', ] - filtering = { + # resource_name = 'storedobject' + ordering = [ + "id", + ] + filtering = { "id": ALL, "store": ALL, "name": ALL, @@ -864,4 +1144,6 @@ class Meta: "doc_rev": ALL, "deleted": ALL, } + + api.doc.register(StoredObjectResource()) diff --git a/ietf/doc/settings.py b/ietf/doc/settings.py index 0a31d9dbae..2b7af6f708 100644 --- a/ietf/doc/settings.py +++ b/ietf/doc/settings.py @@ -1,6 +1,6 @@ CHART_TYPE_COLUMN_OPTIONS = { "chart": { - "type": 'column', + "type": "column", }, "credits": { "enabled": False, @@ -8,30 +8,38 @@ "exporting": { "fallbackToExportServer": False, }, - "rangeSelector" : { + "rangeSelector": { "selected": 5, "allButtonsEnabled": True, }, - "series" : [{ - "name" : "Items", - "type" : "column", - "data" : [], - "dataGrouping": { - "units": [[ - 'week', # unit name - [1,], # allowed multiples - ], [ - 'month', - [1, 4,], - ]] - }, - "turboThreshold": 1, # Only check format of first data point. All others are the same - "pointIntervalUnit": 'day', - "pointPadding": 0.05, - }], - "title" : { - "text" : "Items over time" - }, + "series": [ + { + "name": "Items", + "type": "column", + "data": [], + "dataGrouping": { + "units": [ + [ + "week", # unit name + [ + 1, + ], # allowed multiples + ], + [ + "month", + [ + 1, + 4, + ], + ], + ] + }, + "turboThreshold": 1, # Only check format of first data point. All others are the same + "pointIntervalUnit": "day", + "pointPadding": 0.05, + } + ], + "title": {"text": "Items over time"}, "xAxis": { "type": "datetime", # This makes the axis use the given coordinates, rather than @@ -42,7 +50,7 @@ CHART_TYPE_ACTIVITY_OPTIONS = { "chart": { - "type": 'column', + "type": "column", }, "credits": { "enabled": False, @@ -58,29 +66,35 @@ "navigator": { "enabled": False, }, - "rangeSelector" : { + "rangeSelector": { "enabled": False, }, "scrollbar": { "enabled": False, }, - "series" : [{ - "name" : None, - "animation": False, - "type" : "column", - "data" : [], - "dataGrouping": { - "units": [[ - 'year', # unit name - [1,], # allowed multiples - ]] - }, - "turboThreshold": 1, # Only check format of first data point. All others are the same - "pointIntervalUnit": 'day', - "pointPadding": -0.2, - }], - "title" : { - "text" : None, + "series": [ + { + "name": None, + "animation": False, + "type": "column", + "data": [], + "dataGrouping": { + "units": [ + [ + "year", # unit name + [ + 1, + ], # allowed multiples + ] + ] + }, + "turboThreshold": 1, # Only check format of first data point. All others are the same + "pointIntervalUnit": "day", + "pointPadding": -0.2, + } + ], + "title": { + "text": None, }, "xAxis": { "type": "datetime", @@ -94,4 +108,3 @@ } }, } - diff --git a/ietf/doc/storage_utils.py b/ietf/doc/storage_utils.py index 012efc9071..6e87dd3544 100644 --- a/ietf/doc/storage_utils.py +++ b/ietf/doc/storage_utils.py @@ -28,7 +28,9 @@ def exists_in_storage(kind: str, name: str) -> bool: store = _get_storage(kind) return store.exists_in_storage(kind, name) except Exception as err: - log(f"Blobstore Error: Failed to test existence of {kind}:{name}: {repr(err)}") + log( + f"Blobstore Error: Failed to test existence of {kind}:{name}: {repr(err)}" + ) return False @@ -92,26 +94,34 @@ def store_str( store_bytes(kind, name, content_bytes, allow_overwrite) except Exception as err: # n.b., not likely to get an exception here because store_file or store_bytes will catch it - log(f"Blobstore Error: Failed to store string to {kind}:{name}: {repr(err)}") + log( + f"Blobstore Error: Failed to store string to {kind}:{name}: {repr(err)}" + ) return None def retrieve_bytes(kind: str, name: str) -> bytes: from ietf.doc.storage_backends import maybe_log_timing + content = b"" if settings.ENABLE_BLOBSTORAGE: try: store = _get_storage(kind) with store.open(name) as f: with maybe_log_timing( - hasattr(store, "ietf_log_blob_timing") and store.ietf_log_blob_timing, + hasattr(store, "ietf_log_blob_timing") + and store.ietf_log_blob_timing, "read", - bucket_name=store.bucket_name if hasattr(store, "bucket_name") else "", + bucket_name=( + store.bucket_name if hasattr(store, "bucket_name") else "" + ), name=name, ): content = f.read() except Exception as err: - log(f"Blobstore Error: Failed to read bytes from {kind}:{name}: {repr(err)}") + log( + f"Blobstore Error: Failed to read bytes from {kind}:{name}: {repr(err)}" + ) return content @@ -123,5 +133,7 @@ def retrieve_str(kind: str, name: str) -> str: # TODO-BLOBSTORE: try to decode all the different ways doc.text() does content = content_bytes.decode("utf-8") except Exception as err: - log(f"Blobstore Error: Failed to read string from {kind}:{name}: {repr(err)}") + log( + f"Blobstore Error: Failed to read string from {kind}:{name}: {repr(err)}" + ) return content diff --git a/ietf/doc/tasks.py b/ietf/doc/tasks.py index e24c58e1e7..8812d3b33b 100644 --- a/ietf/doc/tasks.py +++ b/ietf/doc/tasks.py @@ -74,17 +74,19 @@ def expire_last_calls_task(): try: expire_last_call(doc) except Exception: - log.log(f"ERROR: Failed to expire last call for {doc.file_tag()} (id={doc.pk})") + log.log( + f"ERROR: Failed to expire last call for {doc.file_tag()} (id={doc.pk})" + ) else: log.log(f"Expired last call for {doc.file_tag()} (id={doc.pk})") -@shared_task +@shared_task def generate_idnits2_rfc_status_task(): outpath = Path(settings.DERIVED_DIR) / "idnits2-rfc-status" blob = generate_idnits2_rfc_status() try: - outpath.write_text(blob, encoding="utf8") # TODO-BLOBSTORE + outpath.write_text(blob, encoding="utf8") # TODO-BLOBSTORE except Exception as e: log.log(f"failed to write idnits2-rfc-status: {e}") @@ -94,7 +96,7 @@ def generate_idnits2_rfcs_obsoleted_task(): outpath = Path(settings.DERIVED_DIR) / "idnits2-rfcs-obsoleted" blob = generate_idnits2_rfcs_obsoleted() try: - outpath.write_text(blob, encoding="utf8") # TODO-BLOBSTORE + outpath.write_text(blob, encoding="utf8") # TODO-BLOBSTORE except Exception as e: log.log(f"failed to write idnits2-rfcs-obsoleted: {e}") @@ -102,7 +104,7 @@ def generate_idnits2_rfcs_obsoleted_task(): @shared_task def generate_draft_bibxml_files_task(days=7, process_all=False): """Generate bibxml files for recently updated docs - + If process_all is False (the default), processes only docs with new revisions in the last specified number of days. """ @@ -114,7 +116,9 @@ def generate_draft_bibxml_files_task(days=7, process_all=False): doc__type_id="draft", ).order_by("time") if not process_all: - doc_events = doc_events.filter(time__gte=timezone.now() - datetime.timedelta(days=days)) + doc_events = doc_events.filter( + time__gte=timezone.now() - datetime.timedelta(days=days) + ) for event in doc_events: try: update_or_create_draft_bibxml_file(event.doc, event.rev) diff --git a/ietf/doc/templatetags/active_groups_menu.py b/ietf/doc/templatetags/active_groups_menu.py index c60d6dcd1a..1790a9f463 100644 --- a/ietf/doc/templatetags/active_groups_menu.py +++ b/ietf/doc/templatetags/active_groups_menu.py @@ -11,7 +11,9 @@ @register.simple_tag def active_groups_menu(flavor): - parents = GroupTypeName.objects.filter(slug__in=["ag", "area", "rag", "team", "dir", "program", "iabworkshop"]) + parents = GroupTypeName.objects.filter( + slug__in=["ag", "area", "rag", "team", "dir", "program", "iabworkshop"] + ) others = [] for group in Group.objects.filter(acronym__in=("rsoc",), state_id="active"): group.menu_url = reverse("ietf.group.views.group_home", kwargs=dict(acronym=group.acronym)) # type: ignore diff --git a/ietf/doc/templatetags/ballot_icon.py b/ietf/doc/templatetags/ballot_icon.py index a94c145007..751a35a908 100644 --- a/ietf/doc/templatetags/ballot_icon.py +++ b/ietf/doc/templatetags/ballot_icon.py @@ -33,7 +33,7 @@ import datetime -import debug # pyflakes:ignore +import debug # pyflakes:ignore from django import template from django.urls import reverse as urlreverse @@ -48,27 +48,38 @@ register = template.Library() + @register.filter def showballoticon(doc): if doc.type_id == "draft": - if doc.stream_id == 'ietf' and doc.get_state_slug("draft-iesg") not in IESG_BALLOT_ACTIVE_STATES: + if ( + doc.stream_id == "ietf" + and doc.get_state_slug("draft-iesg") not in IESG_BALLOT_ACTIVE_STATES + ): return False - elif doc.stream_id == 'irtf' and doc.get_state_slug("draft-stream-irtf") != "irsgpoll": + elif ( + doc.stream_id == "irtf" + and doc.get_state_slug("draft-stream-irtf") != "irsgpoll" + ): return False - elif doc.stream_id == 'editorial' and doc.get_state_slug("draft-stream-rsab") != "rsabpoll": + elif ( + doc.stream_id == "editorial" + and doc.get_state_slug("draft-stream-rsab") != "rsabpoll" + ): return False elif doc.type_id == "charter": if doc.get_state_slug() not in ("intrev", "extrev", "iesgrev"): return False elif doc.type_id == "conflrev": - if doc.get_state_slug() not in ("iesgeval","defer"): - return False + if doc.get_state_slug() not in ("iesgeval", "defer"): + return False elif doc.type_id == "statchg": - if doc.get_state_slug() not in ("iesgeval","defer", "in-lc"): - return False + if doc.get_state_slug() not in ("iesgeval", "defer", "in-lc"): + return False return True + @register.simple_tag(takes_context=True) def ballot_icon(context, doc): user = context.get("user") @@ -79,7 +90,7 @@ def ballot_icon(context, doc): if not showballoticon(doc): return "" - ballot = doc.ballot if hasattr(doc, 'ballot') else doc.active_ballot() + ballot = doc.ballot if hasattr(doc, "ballot") else doc.active_ballot() if not ballot: return "" @@ -99,15 +110,21 @@ def sort_key(t): request = context.get("request") ballot_edit_return_point_param = f"ballot_edit_return_point={request.path}" - right_click_string = '' + right_click_string = "" if has_role(user, "Area Director"): - right_click_string = 'oncontextmenu="window.location.href=\'{}?{}\';return false;"'.format( - urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=doc.name, ballot_id=ballot.pk)), - ballot_edit_return_point_param) + right_click_string = ( + "oncontextmenu=\"window.location.href='{}?{}';return false;\"".format( + urlreverse( + "ietf.doc.views_ballot.edit_position", + kwargs=dict(name=doc.name, ballot_id=ballot.pk), + ), + ballot_edit_return_point_param, + ) + ) my_blocking = False for i, (balloter, pos) in enumerate(positions): - if user_is_person(user,balloter) and pos and pos.pos.blocking: + if user_is_person(user, balloter) and pos and pos.pos.blocking: my_blocking = True break @@ -118,20 +135,28 @@ def sort_key(t): typename = "RSAB" else: typename = "IESG" - - modal_url = "{}?{}".format( - urlreverse("ietf.doc.views_doc.ballot_popup", kwargs=dict(name=doc.name, ballot_id=ballot.pk)), - ballot_edit_return_point_param) - res = ['') + res.append(">") res.append("") @@ -153,10 +178,14 @@ def sort_key(t): i = i + 1 res.append("") - res.append('' % ballot.pk) + res.append( + '' + % ballot.pk + ) return mark_safe("".join(res)) + @register.filter def ballotposition(doc, user): if not showballoticon(doc) or not has_role(user, "Area Director"): @@ -166,7 +195,12 @@ def ballotposition(doc, user): if not ballot: return None - changed_pos = doc.latest_event(BallotPositionDocEvent, type="changed_ballot_position", balloter__user=user, ballot=ballot) + changed_pos = doc.latest_event( + BallotPositionDocEvent, + type="changed_ballot_position", + balloter__user=user, + ballot=ballot, + ) if changed_pos: pos = changed_pos.pos else: @@ -247,15 +281,17 @@ def state_age_colored(doc): @register.filter def auth48_alert_badge(doc): """Return alert badge, if any, for a document""" - if doc.type_id != 'draft': - return '' + if doc.type_id != "draft": + return "" - iesg_state = doc.get_state_slug('draft-iesg') - if iesg_state != 'rfcqueue': - return '' + iesg_state = doc.get_state_slug("draft-iesg") + if iesg_state != "rfcqueue": + return "" - rfced_state = doc.get_state_slug('draft-rfceditor') - if rfced_state == 'auth48': - return mark_safe('AUTH48') + rfced_state = doc.get_state_slug("draft-rfceditor") + if rfced_state == "auth48": + return mark_safe( + 'AUTH48' + ) - return '' + return "" diff --git a/ietf/doc/templatetags/document_type_badge.py b/ietf/doc/templatetags/document_type_badge.py index a82c606ff9..b62b669102 100644 --- a/ietf/doc/templatetags/document_type_badge.py +++ b/ietf/doc/templatetags/document_type_badge.py @@ -9,7 +9,12 @@ @register.simple_tag def document_type_badge(doc, snapshot, submission, resurrected_by): - context = {"doc": doc, "snapshot": snapshot, "submission": submission, "resurrected_by": resurrected_by} + context = { + "doc": doc, + "snapshot": snapshot, + "submission": submission, + "resurrected_by": resurrected_by, + } if doc.type_id == "rfc": return render_to_string( "doc/badge/doc-badge-rfc.html", @@ -22,7 +27,7 @@ def document_type_badge(doc, snapshot, submission, resurrected_by): ) else: error_message = f"Unsupported document type {doc.type_id}." - if settings.SERVER_MODE != 'production': + if settings.SERVER_MODE != "production": raise ValueError(error_message) else: log(error_message) diff --git a/ietf/doc/templatetags/ietf_filters.py b/ietf/doc/templatetags/ietf_filters.py index d4adf96a27..f7f44223da 100644 --- a/ietf/doc/templatetags/ietf_filters.py +++ b/ietf/doc/templatetags/ietf_filters.py @@ -11,7 +11,12 @@ from django import template from django.conf import settings from django.utils.html import escape -from django.template.defaultfilters import truncatewords_html, linebreaksbr, stringfilter, striptags +from django.template.defaultfilters import ( + truncatewords_html, + linebreaksbr, + stringfilter, + striptags, +) from django.utils.safestring import mark_safe, SafeData from django.utils.html import strip_tags from django.utils.encoding import force_str @@ -21,11 +26,13 @@ from django.urls import NoReverseMatch from django.utils import timezone -import debug # pyflakes:ignore +import debug # pyflakes:ignore from ietf.doc.models import BallotDocEvent, Document from ietf.doc.models import ConsensusDocEvent -from ietf.ietfauth.utils import can_request_rfc_publication as utils_can_request_rfc_publication +from ietf.ietfauth.utils import ( + can_request_rfc_publication as utils_can_request_rfc_publication, +) from ietf.utils import log from ietf.doc.utils import prettify_std_name from ietf.utils.html import clean_html @@ -34,25 +41,31 @@ register = template.Library() + def collapsebr(html): - return re.sub('(<(br ?/|/p)>[ \n]*)(<(br) ?/?>[ \n]*)*(<(br|p) ?/?>[ \n]*)', '\\1\\5', html) + return re.sub( + "(<(br ?/|/p)>[ \n]*)(<(br) ?/?>[ \n]*)*(<(br|p) ?/?>[ \n]*)", "\\1\\5", html + ) + @register.filter def indent(value, numspaces=2): replacement = "\n" + " " * int(numspaces) res = value.replace("\n", replacement) if res.endswith(replacement): - res = res[:-int(numspaces)] # fix up superfluous spaces + res = res[: -int(numspaces)] # fix up superfluous spaces return res + @register.filter def unindent(value): """Remove indentation from string.""" return re.sub("\n +", "\n", value) + # there's an "ahref -> a href" in GEN_UTIL # but let's wait until we understand what that's for. -@register.filter(name='make_one_per_line') +@register.filter(name="make_one_per_line") def make_one_per_line(value): """ Turn a comma-separated list into a carriage-return-seperated list. @@ -71,18 +84,20 @@ def make_one_per_line(value): if value and isinstance(value, str): return re.sub(", ?", "\n", value) elif value and isinstance(value, bytes): - log.assertion('isinstance(value, str)') + log.assertion("isinstance(value, str)") else: return value -@register.filter(name='keep_spacing') + +@register.filter(name="keep_spacing") def keep_spacing(value): """ Replace any two spaces with one   and one space so that HTML output doesn't collapse them.""" - return value.replace(' ', '  ') + return value.replace(" ", "  ") -@register.filter(name='format_textarea') + +@register.filter(name="format_textarea") def format_textarea(value): """ Escapes HTML, except for , ,
. @@ -90,9 +105,17 @@ def format_textarea(value): Adds
at the end like the builtin linebreaksbr. Also calls keep_spacing.""" - return keep_spacing(linebreaksbr(escape(value).replace('<b>','').replace('</b>','').replace('<br>','
'))) + return keep_spacing( + linebreaksbr( + escape(value) + .replace("<b>", "") + .replace("</b>", "") + .replace("<br>", "
") + ) + ) -@register.filter(name='sanitize') + +@register.filter(name="sanitize") def sanitize(value): """Sanitizes an HTML fragment. This means both fixing broken html and restricting elements and @@ -103,15 +126,15 @@ def sanitize(value): # For use with ballot view -@register.filter(name='bracket') +@register.filter(name="bracket") def square_brackets(value): """Adds square brackets around text.""" if isinstance(value, str): if value == "": - value = " " + value = " " return "[ %s ]" % value elif isinstance(value, bytes): - log.assertion('isinstance(value, str)') + log.assertion("isinstance(value, str)") elif value > 0: return "[ X ]" elif value < 0: @@ -119,26 +142,31 @@ def square_brackets(value): else: return "[ ]" -@register.filter(name='bracketpos') -def bracketpos(pos,posslug): - if pos.pos.slug==posslug: + +@register.filter(name="bracketpos") +def bracketpos(pos, posslug): + if pos.pos.slug == posslug: return "[ X ]" elif posslug in [x.slug for x in pos.old_positions]: return "[ . ]" else: return "[ ]" -register.filter('fill', fill) + +register.filter("fill", fill) + @register.filter def prettystdname(string, space=" "): from ietf.doc.utils import prettify_std_name + return prettify_std_name(force_str(string or ""), space) + @register.filter -def rfceditor_info_url(rfcnum : str): +def rfceditor_info_url(rfcnum: str): """Link to the RFC editor info page for an RFC""" - return urljoin(settings.RFC_EDITOR_INFO_BASE_URL, f'rfc{rfcnum}') + return urljoin(settings.RFC_EDITOR_INFO_BASE_URL, f"rfc{rfcnum}") def doc_name(name): @@ -151,7 +179,7 @@ def find_unique(n): exact = Document.objects.filter(name=n).first() found = exact.name if exact else "_" # TODO review this cache policy (and the need for these entire function) - cache.set(key, found, timeout=60*60*24) # cache for one day + cache.set(key, found, timeout=60 * 60 * 24) # cache for one day return None if found == "_" else found # chop away extension @@ -229,6 +257,7 @@ def link_other_doc_match(match): url = urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc + rev)) return f'{match[1]}' + @register.filter(name="urlize_ietf_docs", is_safe=True, needs_autoescape=True) def urlize_ietf_docs(string, autoescape=None): """ @@ -263,60 +292,81 @@ def urlize_ietf_docs(string, autoescape=None): urlize_ietf_docs = stringfilter(urlize_ietf_docs) -@register.filter(name='urlize_related_source_list', is_safe=True, document_html=False) + +@register.filter(name="urlize_related_source_list", is_safe=True, document_html=False) def urlize_related_source_list(related, document_html=False): """Convert a list of RelatedDocuments into list of links using the source document's canonical name""" links = [] names = set() titles = set() for rel in related: - name=rel.source.name + name = rel.source.name title = rel.source.title if name in names and title in titles: continue names.add(name) titles.add(title) - url = urlreverse('ietf.doc.views_doc.document_main' if document_html is False else 'ietf.doc.views_doc.document_html', kwargs=dict(name=name)) + url = urlreverse( + ( + "ietf.doc.views_doc.document_main" + if document_html is False + else "ietf.doc.views_doc.document_html" + ), + kwargs=dict(name=name), + ) name = escape(name) title = escape(title) - links.append(mark_safe( - '%(name)s' % dict(name=prettify_std_name(name), - title=title, - url=url) - )) + links.append( + mark_safe( + '%(name)s' + % dict(name=prettify_std_name(name), title=title, url=url) + ) + ) return links - -@register.filter(name='urlize_related_target_list', is_safe=True, document_html=False) + + +@register.filter(name="urlize_related_target_list", is_safe=True, document_html=False) def urlize_related_target_list(related, document_html=False): """Convert a list of RelatedDocuments into list of links using the target document's canonical name""" links = [] for rel in related: - name=rel.target.name + name = rel.target.name title = rel.target.title - url = urlreverse('ietf.doc.views_doc.document_main' if document_html is False else 'ietf.doc.views_doc.document_html', kwargs=dict(name=name)) + url = urlreverse( + ( + "ietf.doc.views_doc.document_main" + if document_html is False + else "ietf.doc.views_doc.document_html" + ), + kwargs=dict(name=name), + ) name = escape(name) title = escape(title) - links.append(mark_safe( - '%(name)s' % dict(name=prettify_std_name(name), - title=title, - url=url) - )) + links.append( + mark_safe( + '%(name)s' + % dict(name=prettify_std_name(name), title=title, url=url) + ) + ) return links - -@register.filter(name='dashify') + + +@register.filter(name="dashify") def dashify(string): """ Replace each character in string with '-', to produce an underline effect for plain text files. """ - return re.sub('.', '-', string) + return re.sub(".", "-", string) + @register.filter def underline(string): """Return string with an extra line underneath of dashes, for plain text underlining.""" return string + "\n" + ("-" * len(string)) -@register.filter(name='timesince_days') + +@register.filter(name="timesince_days") def timesince_days(date): """Returns the number of days since 'date' (relative to now) @@ -329,24 +379,30 @@ def timesince_days(date): """ if date.__class__ is not datetime.datetime: - date = datetime.datetime(date.year, date.month, date.day, tzinfo=ZoneInfo(settings.TIME_ZONE)) + date = datetime.datetime( + date.year, date.month, date.day, tzinfo=ZoneInfo(settings.TIME_ZONE) + ) delta = timezone.now() - date return delta.days + @register.filter def split(text, splitter=None): return text.split(splitter) + register.filter("maybewordwrap", stringfilter(wrap_text_if_unwrapped)) register.filter("wordwrap", stringfilter(wordwrap)) + @register.filter(name="compress_empty_lines") def compress_empty_lines(text): text = re.sub("( *\n){3,}", "\n\n", text) return text -@register.filter(name='linebreaks_crlf') + +@register.filter(name="linebreaks_crlf") def linebreaks_crlf(text): """ Normalize all linebreaks to CRLF. @@ -359,7 +415,8 @@ def linebreaks_crlf(text): text = text.replace("\n", "\r\n") return text -@register.filter(name='linebreaks_lf') + +@register.filter(name="linebreaks_lf") def linebreaks_lf(text): """ Normalize all linebreaks to LF. @@ -370,7 +427,8 @@ def linebreaks_lf(text): text = text.replace("\r", "\n") return text -@register.filter(name='clean_whitespace') + +@register.filter(name="clean_whitespace") def clean_whitespace(text): """ Map all ASCII control characters (0x00-0x1F) to spaces, and @@ -379,7 +437,8 @@ def clean_whitespace(text): text = re.sub("[\000-\040]+", " ", text) return text.strip() -@register.filter(name='unescape') + +@register.filter(name="unescape") def unescape(text): """ Unescape  />/< @@ -391,31 +450,34 @@ def unescape(text): text = text.replace("
", "\n") return text -@register.filter(name='new_enough') -def new_enough(x,request): + +@register.filter(name="new_enough") +def new_enough(x, request): days = int(settings.USER_PREFERENCE_DEFAULTS["new_enough"]) value = request.COOKIES.get("new_enough", None) if value and value.isdigit(): days = int(value) return x < days -@register.filter(name='expires_soon') -def expires_soon(x,request): + +@register.filter(name="expires_soon") +def expires_soon(x, request): days = int(settings.USER_PREFERENCE_DEFAULTS["expires_soon"]) value = request.COOKIES.get("expires_soon", None) if value and value.isdigit(): days = int(value) return x > -days -@register.filter(name='startswith') + +@register.filter(name="startswith") def startswith(x, y): return str(x).startswith(y) -@register.filter(name='removeprefix', is_safe=False) +@register.filter(name="removeprefix", is_safe=False) def removeprefix(value, prefix): """Remove an exact-match prefix - + The is_safe flag is False because indiscriminate use of this could result in non-safe output. See https://docs.djangoproject.com/en/2.2/howto/custom-template-tags/#filters-and-auto-escaping which describes the possibility that removing characters from an escaped string may introduce @@ -423,7 +485,7 @@ def removeprefix(value, prefix): """ base = str(value) if base.startswith(prefix): - return base[len(prefix):] + return base[len(prefix) :] else: return base @@ -431,19 +493,25 @@ def removeprefix(value, prefix): @register.filter def has_role(user, role_names): from ietf.ietfauth.utils import has_role + if not user: return False - return has_role(user, role_names.split(',')) + return has_role(user, role_names.split(",")) + @register.filter def ad_area(user): if user and user.is_authenticated: from ietf.group.models import Group - g = Group.objects.filter(role__name__in=("pre-ad", "ad"), role__person__user=user) + + g = Group.objects.filter( + role__name__in=("pre-ad", "ad"), role__person__user=user + ) if g: return g[0].acronym return None + @register.filter def format_history_text(text, trunc_words=25): """Run history text through some cleaning and add ellipsis if it's too long.""" @@ -452,20 +520,29 @@ def format_history_text(text, trunc_words=25): return format_snippet(full, trunc_words) + @register.filter -def format_snippet(text, trunc_words=25): +def format_snippet(text, trunc_words=25): # urlize if there aren't already links present text = linkify(text) full = keep_spacing(collapsebr(linebreaksbr(mark_safe(clean_html(text))))) snippet = truncatewords_html(full, trunc_words) if snippet != full: - return mark_safe('
%s
%s
' % (snippet, full)) + return mark_safe( + '
%s
%s
' + % (snippet, full) + ) return mark_safe(full) + @register.simple_tag def doc_edit_button(url_name, *args, **kwargs): """Given URL name/args/kwargs, looks up the URL just like "url" tag and returns a properly formatted button for the document material tables.""" - return mark_safe('Edit' % (urlreverse(url_name, args=args, kwargs=kwargs))) + return mark_safe( + 'Edit' + % (urlreverse(url_name, args=args, kwargs=kwargs)) + ) + @register.filter def textify(text): @@ -474,9 +551,10 @@ def textify(text): # There are probably additional conversions we should apply here return text + @register.filter def state(doc, slug): - if slug == "stream": # convenient shorthand + if slug == "stream": # convenient shorthand slug = "%s-stream-%s" % (doc.type_id, doc.stream_id) return doc.get_state(slug) @@ -490,37 +568,55 @@ def is_unexpected_wg_state(doc): draft_iesg_state = doc.get_state("draft-iesg") draft_stream_state = doc.get_state("draft-stream-ietf") - return draft_iesg_state.slug != "idexists" and draft_stream_state is not None and draft_stream_state.slug != "sub-pub" + return ( + draft_iesg_state.slug != "idexists" + and draft_stream_state is not None + and draft_stream_state.slug != "sub-pub" + ) @register.filter def statehelp(state): "Output help icon with tooltip for state." from django.urls import reverse as urlreverse + tooltip = escape(strip_tags(state.desc)) - url = urlreverse('ietf.doc.views_help.state_help', kwargs=dict(type=state.type_id)) + "#" + state.slug - return mark_safe('?' % (url, tooltip)) + url = ( + urlreverse("ietf.doc.views_help.state_help", kwargs=dict(type=state.type_id)) + + "#" + + state.slug + ) + return mark_safe( + '?' % (url, tooltip) + ) + @register.filter def sectionlevel(section_number): return section_number.count(".") + 1 + def _test(): import doctest + doctest.testmod() + if __name__ == "__main__": _test() + @register.filter -def plural(text, seq, arg='s'): +def plural(text, seq, arg="s"): "Similar to pluralize, but looks at the text, too" from django.template.defaultfilters import pluralize - if text.endswith('s'): + + if text.endswith("s"): return text else: return text + pluralize(len(seq), arg) + @register.filter def ics_esc(text): text = re.sub(r"([\n,;\\])", r"\\\1", text) @@ -552,12 +648,13 @@ def ics_date_time(dt, tzname): >>> ics_date_time(datetime.datetime(2022,1,2,3,4,5), 'America/Los_Angeles') ';TZID=America/Los_Angeles:20220102T030405' """ - timestamp = dt.strftime('%Y%m%dT%H%M%S') - if tzname.lower() in ('gmt', 'utc'): - return f':{timestamp}Z' + timestamp = dt.strftime("%Y%m%dT%H%M%S") + if tzname.lower() in ("gmt", "utc"): + return f":{timestamp}Z" else: - return f';TZID={ics_esc(tzname)}:{timestamp}' - + return f";TZID={ics_esc(tzname)}:{timestamp}" + + @register.filter def next_day(value): return value + datetime.timedelta(days=1) @@ -566,7 +663,7 @@ def next_day(value): @register.filter def consensus(doc): """Returns document consensus Yes/No/Unknown.""" - event = doc.latest_event(ConsensusDocEvent,type="changed_consensus") + event = doc.latest_event(ConsensusDocEvent, type="changed_consensus") if event: if event.consensus: return "Yes" @@ -592,32 +689,33 @@ def std_level_to_label_format(doc): def pos_to_label_format(text): """Returns valid Bootstrap classes to label a ballot position.""" return { - 'Yes': 'bg-yes text-light', - 'No Objection': 'bg-noobj text-dark', - 'Abstain': 'bg-abstain text-light', - 'Discuss': 'bg-discuss text-light', - 'Block': 'bg-discuss text-light', - 'Recuse': 'bg-recuse text-light', - 'Not Ready': 'bg-discuss text-light', - 'Need More Time': 'bg-discuss text-light', - 'Concern': 'bg-discuss text-light', + "Yes": "bg-yes text-light", + "No Objection": "bg-noobj text-dark", + "Abstain": "bg-abstain text-light", + "Discuss": "bg-discuss text-light", + "Block": "bg-discuss text-light", + "Recuse": "bg-recuse text-light", + "Not Ready": "bg-discuss text-light", + "Need More Time": "bg-discuss text-light", + "Concern": "bg-discuss text-light", + }.get(str(text), "bg-norecord text-dark") - }.get(str(text), 'bg-norecord text-dark') @register.filter def pos_to_border_format(text): """Returns valid Bootstrap classes to label a ballot position border.""" return { - 'Yes': 'border-yes', - 'No Objection': 'border-noobj', - 'Abstain': 'border-abstain', - 'Discuss': 'border-discuss', - 'Block': 'border-discuss', - 'Recuse': 'border-recuse', - 'Not Ready': 'border-discuss', - 'Need More Time': 'border-discuss', - 'Concern': 'border-discuss', - }.get(str(text), 'border-norecord') + "Yes": "border-yes", + "No Objection": "border-noobj", + "Abstain": "border-abstain", + "Discuss": "border-discuss", + "Block": "border-discuss", + "Recuse": "border-recuse", + "Not Ready": "border-discuss", + "Need More Time": "border-discuss", + "Concern": "border-discuss", + }.get(str(text), "border-norecord") + @register.filter def capfirst_allcaps(text): @@ -633,6 +731,7 @@ def capfirst_allcaps(text): result = result.replace(token, token.lower()) return result + @register.filter def lower_allcaps(text): """Like lower, except it doesn't lowercase words in ALL CAPS.""" @@ -642,75 +741,98 @@ def lower_allcaps(text): result = result.replace(token, token.lower()) return result + @register.filter def document_content(doc): if doc is None: return None - content = doc.text_or_error() # pyflakes:ignore + content = doc.text_or_error() # pyflakes:ignore return content + @register.filter def format_timedelta(timedelta): s = timedelta.seconds hours, remainder = divmod(s, 3600) minutes, seconds = divmod(remainder, 60) - return '{hours:02d}:{minutes:02d}'.format(hours=hours,minutes=minutes) + return "{hours:02d}:{minutes:02d}".format(hours=hours, minutes=minutes) + @register.filter() def comma_separated_list(seq, end_word="and"): if len(seq) < 2: return "".join(seq) else: - return ", ".join(seq[:-1]) + " %s %s"%(end_word, seq[-1]) + return ", ".join(seq[:-1]) + " %s %s" % (end_word, seq[-1]) + @register.filter() def zaptmp(s): - return re.sub(r'/tmp/tmp[^/]+/', '', s) + return re.sub(r"/tmp/tmp[^/]+/", "", s) + @register.filter() def rfcbis(s): - m = re.search(r'^.*-rfc(\d+)-?bis(-.*)?$', s) - return None if m is None else 'rfc' + m.group(1) + m = re.search(r"^.*-rfc(\d+)-?bis(-.*)?$", s) + return None if m is None else "rfc" + m.group(1) + @register.filter @stringfilter def urlize(value): raise RuntimeError("Use linkify from textfilters instead of urlize") - + + @register.filter @stringfilter def charter_major_rev(rev): return rev[:2] + @register.filter @stringfilter def charter_minor_rev(rev): return rev[3:5] + @register.filter() -def can_defer(user,doc): +def can_defer(user, doc): ballot = doc.latest_event(BallotDocEvent, type="created_ballot") - if ballot and (doc.type_id == "draft" or doc.type_id == "conflrev" or doc.type_id=="statchg") and doc.stream_id == 'ietf' and has_role(user, 'Area Director,Secretariat'): + if ( + ballot + and ( + doc.type_id == "draft" + or doc.type_id == "conflrev" + or doc.type_id == "statchg" + ) + and doc.stream_id == "ietf" + and has_role(user, "Area Director,Secretariat") + ): return True else: return False + @register.filter() def can_clear_ballot(user, doc): return can_defer(user, doc) + @register.filter() def can_request_rfc_publication(user, doc): return utils_can_request_rfc_publication(user, doc) + @register.filter() -def can_ballot(user,doc): +def can_ballot(user, doc): if doc.stream_id == "irtf" and doc.type_id == "draft": - return has_role(user,"IRSG Member") + return has_role(user, "IRSG Member") elif doc.stream_id == "editorial" and doc.type_id == "draft": - return has_role(user,"RSAB Member") + return has_role(user, "RSAB Member") else: - return user.person.role_set.filter(name="ad", group__type="area", group__state="active") + return user.person.role_set.filter( + name="ad", group__type="area", group__state="active" + ) @register.filter @@ -764,7 +886,8 @@ def is_regular_agenda_item(assignment): >>> is_regular_agenda_item(None) False """ - return assignment is not None and assignment.slot_type().slug == 'regular' + return assignment is not None and assignment.slot_type().slug == "regular" + @register.filter def is_plenary_agenda_item(assignment): @@ -785,7 +908,8 @@ def is_plenary_agenda_item(assignment): >>> is_plenary_agenda_item(None) False """ - return assignment is not None and assignment.slot_type().slug == 'plenary' + return assignment is not None and assignment.slot_type().slug == "plenary" + @register.filter def is_special_agenda_item(assignment): @@ -807,12 +931,13 @@ def is_special_agenda_item(assignment): False """ return assignment is not None and assignment.slot_type().slug in [ - 'break', - 'reg', - 'other', - 'officehours', + "break", + "reg", + "other", + "officehours", ] + @register.filter def should_show_agenda_session_buttons(assignment): """Should this agenda item show the session buttons (chat link, etc)? @@ -841,7 +966,7 @@ def should_show_agenda_session_buttons(assignment): return False num = assignment.meeting().number if num.isdigit() and int(num) <= settings.MEETING_LEGACY_OFFICE_HOURS_END: - return not assignment.session.name.lower().endswith(' office hours') + return not assignment.session.name.lower().endswith(" office hours") else: return True @@ -900,6 +1025,7 @@ def badgeify(blob): return text + @register.filter def simple_history_delta_changes(history): """Returns diff between given history and previous entry.""" @@ -909,6 +1035,7 @@ def simple_history_delta_changes(history): return delta.changes return [] + @register.filter def simple_history_delta_change_cnt(history): """Returns number of changes between given history and previous entry.""" @@ -918,15 +1045,20 @@ def simple_history_delta_change_cnt(history): return len(delta.changes) return 0 + @register.filter def mtime(path): """Returns a datetime object representing mtime given a pathlib Path object""" - return datetime.datetime.fromtimestamp(path.stat().st_mtime).astimezone(ZoneInfo(settings.TIME_ZONE)) + return datetime.datetime.fromtimestamp(path.stat().st_mtime).astimezone( + ZoneInfo(settings.TIME_ZONE) + ) + @register.filter def mtime_is_epoch(path): return path.stat().st_mtime == 0 + @register.filter def url_for_path(path): """Consructs a 'best' URL for web access to the given pathlib Path object. diff --git a/ietf/doc/templatetags/mail_filters.py b/ietf/doc/templatetags/mail_filters.py index 6be6620315..07bfca80fb 100644 --- a/ietf/doc/templatetags/mail_filters.py +++ b/ietf/doc/templatetags/mail_filters.py @@ -2,24 +2,25 @@ register = template.Library() -@register.filter(name='std_level_prompt') + +@register.filter(name="std_level_prompt") def std_level_prompt(doc): """ Returns the name from the std level names table corresponding to the object's intended_std_level (with the word RFC appended in some cases), or a prompt requesting that the intended_std_level be set.""" - + prompt = "*** YOU MUST SELECT AN INTENDED STATUS FOR THIS INTERNET-DRAFT AND REGENERATE THIS TEXT ***" if doc.intended_std_level: - prompt = doc.intended_std_level.name - if doc.intended_std_level_id in ('inf','exp','hist'): - prompt = prompt + " RFC" + prompt = doc.intended_std_level.name + if doc.intended_std_level_id in ("inf", "exp", "hist"): + prompt = prompt + " RFC" return prompt -@register.filter(name='std_level_prompt_with_article') +@register.filter(name="std_level_prompt_with_article") def std_level_prompt_with_article(doc): """ Returns the standard level prompt prefixed with an appropriate article.""" @@ -29,8 +30,7 @@ def std_level_prompt_with_article(doc): # Grammar war alert: This will generate "an historic" article = "" if doc.intended_std_level: - article = "a" - if doc.intended_std_level.name[0].lower() in "aehiou": - article = "an" - return article+" "+std_level_prompt(doc) - + article = "a" + if doc.intended_std_level.name[0].lower() in "aehiou": + article = "an" + return article + " " + std_level_prompt(doc) diff --git a/ietf/doc/templatetags/managed_groups.py b/ietf/doc/templatetags/managed_groups.py index b291fc324b..1eca3fc4ac 100644 --- a/ietf/doc/templatetags/managed_groups.py +++ b/ietf/doc/templatetags/managed_groups.py @@ -4,35 +4,42 @@ from django import template -import debug # pyflakes:ignore +import debug # pyflakes:ignore from ietf.group.models import Group from ietf.group.utils import group_features_group_filter register = template.Library() + @register.filter def docman_groups(user): if not (user and hasattr(user, "is_authenticated") and user.is_authenticated): return [] - groups = Group.objects.filter( role__person=user.person, - type__features__has_documents=True, - state__slug__in=('active', 'bof')) - groups = group_features_group_filter(groups, user.person, 'docman_roles') + groups = Group.objects.filter( + role__person=user.person, + type__features__has_documents=True, + state__slug__in=("active", "bof"), + ) + groups = group_features_group_filter(groups, user.person, "docman_roles") return groups + @register.filter def matman_groups(user): if not (user and hasattr(user, "is_authenticated") and user.is_authenticated): return [] - groups = Group.objects.filter( role__person=user.person, - type__features__has_session_materials=True, - state__slug__in=('active', 'bof')) - groups = group_features_group_filter(groups, user.person, 'matman_roles') + groups = Group.objects.filter( + role__person=user.person, + type__features__has_session_materials=True, + state__slug__in=("active", "bof"), + ) + groups = group_features_group_filter(groups, user.person, "matman_roles") return groups + @register.filter def managed_review_groups(user): if not (user and hasattr(user, "is_authenticated") and user.is_authenticated): @@ -40,11 +47,13 @@ def managed_review_groups(user): groups = [] - groups.extend(Group.objects.filter( - role__name__slug='secr', - role__person__user=user, - reviewteamsettings__isnull=False, - state__slug='active').select_related("type")) + groups.extend( + Group.objects.filter( + role__name__slug="secr", + role__person__user=user, + reviewteamsettings__isnull=False, + state__slug="active", + ).select_related("type") + ) return groups - diff --git a/ietf/doc/templatetags/tests_ballot_icon.py b/ietf/doc/templatetags/tests_ballot_icon.py index fa0ee3eed9..45b2702f1c 100644 --- a/ietf/doc/templatetags/tests_ballot_icon.py +++ b/ietf/doc/templatetags/tests_ballot_icon.py @@ -5,30 +5,36 @@ class BallotIconTests(TestCase): def test_auth48_alert_badge_marks_auth48(self): - draft = WgDraftFactory(states=[ - ('draft','active'), - ('draft-iesg','rfcqueue'), - ('draft-rfceditor', 'auth48'), - ]) + draft = WgDraftFactory( + states=[ + ("draft", "active"), + ("draft-iesg", "rfcqueue"), + ("draft-rfceditor", "auth48"), + ] + ) output = auth48_alert_badge(draft) - self.assertIn('AUTH48', output) + self.assertIn("AUTH48", output) def test_auth48_alert_badge_ignores_others(self): # If the auth48_alert_badge() method becomes more complicated, more # sophisticated testing can be added. # For now, just test a couple states that should not be marked. - draft = WgDraftFactory(states=[ - ('draft', 'active'), - ('draft-iesg', 'approved'), # not in rfcqueue state - ('draft-rfceditor', 'auth48'), - ]) + draft = WgDraftFactory( + states=[ + ("draft", "active"), + ("draft-iesg", "approved"), # not in rfcqueue state + ("draft-rfceditor", "auth48"), + ] + ) output = auth48_alert_badge(draft) - self.assertEqual('', output) + self.assertEqual("", output) - draft = WgDraftFactory(states=[ - ('draft', 'active'), - ('draft-iesg', 'rfcqueue'), - ('draft-rfceditor', 'auth48-done'), # not in auth48 state - ]) + draft = WgDraftFactory( + states=[ + ("draft", "active"), + ("draft-iesg", "rfcqueue"), + ("draft-rfceditor", "auth48-done"), # not in auth48 state + ] + ) output = auth48_alert_badge(draft) - self.assertEqual('', output) + self.assertEqual("", output) diff --git a/ietf/doc/templatetags/tests_ietf_filters.py b/ietf/doc/templatetags/tests_ietf_filters.py index b5130849ea..987560433b 100644 --- a/ietf/doc/templatetags/tests_ietf_filters.py +++ b/ietf/doc/templatetags/tests_ietf_filters.py @@ -176,17 +176,44 @@ def test_urlize_ietf_docs(self): for input, output in cases: # debug.show("(input, urlize_ietf_docs(input), output)") self.assertEqual(urlize_ietf_docs(input), output) - + def test_is_unexpected_wg_state(self): """ Test that the unexpected_wg_state function works correctly """ # test documents with expected wg states self.assertFalse(is_unexpected_wg_state(RfcFactory())) - self.assertFalse(is_unexpected_wg_state(WgDraftFactory (states=[('draft-stream-ietf', 'sub-pub')]))) - self.assertFalse(is_unexpected_wg_state(WgDraftFactory (states=[('draft-iesg', 'idexists')]))) - self.assertFalse(is_unexpected_wg_state(WgDraftFactory (states=[('draft-stream-ietf', 'wg-cand'), ('draft-iesg','idexists')]))) + self.assertFalse( + is_unexpected_wg_state( + WgDraftFactory(states=[("draft-stream-ietf", "sub-pub")]) + ) + ) + self.assertFalse( + is_unexpected_wg_state(WgDraftFactory(states=[("draft-iesg", "idexists")])) + ) + self.assertFalse( + is_unexpected_wg_state( + WgDraftFactory( + states=[ + ("draft-stream-ietf", "wg-cand"), + ("draft-iesg", "idexists"), + ] + ) + ) + ) # test documents with unexpected wg states due to invalid combination of states - self.assertTrue(is_unexpected_wg_state(WgDraftFactory (states=[('draft-stream-ietf', 'wg-cand'), ('draft-iesg','lc-req')]))) - self.assertTrue(is_unexpected_wg_state(WgDraftFactory (states=[('draft-stream-ietf', 'chair-w'), ('draft-iesg','pub-req')]))) + self.assertTrue( + is_unexpected_wg_state( + WgDraftFactory( + states=[("draft-stream-ietf", "wg-cand"), ("draft-iesg", "lc-req")] + ) + ) + ) + self.assertTrue( + is_unexpected_wg_state( + WgDraftFactory( + states=[("draft-stream-ietf", "chair-w"), ("draft-iesg", "pub-req")] + ) + ) + ) diff --git a/ietf/doc/templatetags/wg_menu.py b/ietf/doc/templatetags/wg_menu.py index 3e8d209448..aed20a158e 100644 --- a/ietf/doc/templatetags/wg_menu.py +++ b/ietf/doc/templatetags/wg_menu.py @@ -32,7 +32,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import debug # pyflakes: ignore +import debug # pyflakes: ignore from django import template from django.template.loader import render_to_string @@ -78,6 +78,4 @@ def wg_menu(flavor): elif p.acronym == "rfceditor": p.menu_url = "/rfcedtyp/" - return render_to_string( - "base/menu_wg.html", {"parents": parents, "flavor": flavor} - ) + return render_to_string("base/menu_wg.html", {"parents": parents, "flavor": flavor}) diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index d74688f3f6..94fdcaf4c3 100644 --- a/ietf/doc/tests.py +++ b/ietf/doc/tests.py @@ -34,16 +34,41 @@ from weasyprint.urls import URLFetchingError -import debug # pyflakes:ignore - -from ietf.doc.models import ( Document, DocRelationshipName, RelatedDocument, State, - DocEvent, BallotPositionDocEvent, LastCallDocEvent, WriteupDocEvent, NewRevisionDocEvent, BallotType, - EditedAuthorsDocEvent, StateType) -from ietf.doc.factories import ( DocumentFactory, DocEventFactory, CharterFactory, - ConflictReviewFactory, WgDraftFactory, IndividualDraftFactory, WgRfcFactory, - IndividualRfcFactory, StateDocEventFactory, BallotPositionDocEventFactory, - BallotDocEventFactory, DocumentAuthorFactory, NewRevisionDocEventFactory, - StatusChangeFactory, DocExtResourceFactory, RgDraftFactory, BcpFactory) +import debug # pyflakes:ignore + +from ietf.doc.models import ( + Document, + DocRelationshipName, + RelatedDocument, + State, + DocEvent, + BallotPositionDocEvent, + LastCallDocEvent, + WriteupDocEvent, + NewRevisionDocEvent, + BallotType, + EditedAuthorsDocEvent, + StateType, +) +from ietf.doc.factories import ( + DocumentFactory, + DocEventFactory, + CharterFactory, + ConflictReviewFactory, + WgDraftFactory, + IndividualDraftFactory, + WgRfcFactory, + IndividualRfcFactory, + StateDocEventFactory, + BallotPositionDocEventFactory, + BallotDocEventFactory, + DocumentAuthorFactory, + NewRevisionDocEventFactory, + StatusChangeFactory, + DocExtResourceFactory, + RgDraftFactory, + BcpFactory, +) from ietf.doc.forms import NotifyForm from ietf.doc.fields import SearchableDocumentsField from ietf.doc.utils import ( @@ -60,10 +85,19 @@ from ietf.group.factories import GroupFactory, RoleFactory from ietf.ipr.factories import HolderIprDisclosureFactory from ietf.meeting.models import Meeting, SessionPresentation, SchedulingEvent -from ietf.meeting.factories import ( MeetingFactory, SessionFactory, SessionPresentationFactory, - ProceedingsMaterialFactory ) +from ietf.meeting.factories import ( + MeetingFactory, + SessionFactory, + SessionPresentationFactory, + ProceedingsMaterialFactory, +) -from ietf.name.models import SessionStatusName, BallotPositionName, DocTypeName, RoleName +from ietf.name.models import ( + SessionStatusName, + BallotPositionName, + DocTypeName, + RoleName, +) from ietf.person.models import Person from ietf.person.factories import PersonFactory, EmailFactory from ietf.utils.mail import outbox, empty_outbox @@ -77,13 +111,24 @@ class SearchTests(TestCase): def test_search(self): - draft = WgDraftFactory(name='draft-ietf-mars-test',group=GroupFactory(acronym='mars',parent=Group.objects.get(acronym='farfut')),authors=[PersonFactory()],ad=PersonFactory()) + draft = WgDraftFactory( + name="draft-ietf-mars-test", + group=GroupFactory( + acronym="mars", parent=Group.objects.get(acronym="farfut") + ), + authors=[PersonFactory()], + ad=PersonFactory(), + ) rfc = WgRfcFactory() draft.set_state(State.objects.get(used=True, type="draft-iesg", slug="pub-req")) - old_draft = IndividualDraftFactory(name='draft-foo-mars-test',authors=[PersonFactory()],title="Optimizing Martian Network Topologies") + old_draft = IndividualDraftFactory( + name="draft-foo-mars-test", + authors=[PersonFactory()], + title="Optimizing Martian Network Topologies", + ) old_draft.set_state(State.objects.get(used=True, type="draft", slug="expired")) - base_url = urlreverse('ietf.doc.views_search.search') + base_url = urlreverse("ietf.doc.views_search.search") # only show form, no search yet r = self.client.get(base_url) @@ -126,35 +171,50 @@ def test_search(self): r = self.client.get(base_url + "?olddrafts=on&name=%s" % draft.name) self.assertEqual(r.status_code, 200) self.assertContains(r, draft.title) - + draft.set_state(State.objects.get(type="draft", slug="active")) # find by title - r = self.client.get(base_url + "?activedrafts=on&name=%s" % draft.title.split()[0]) + r = self.client.get( + base_url + "?activedrafts=on&name=%s" % draft.title.split()[0] + ) self.assertEqual(r.status_code, 200) self.assertContains(r, draft.title) # find by author - r = self.client.get(base_url + "?activedrafts=on&by=author&author=%s" % draft.documentauthor_set.first().person.name_parts()[1]) + r = self.client.get( + base_url + + "?activedrafts=on&by=author&author=%s" + % draft.documentauthor_set.first().person.name_parts()[1] + ) self.assertEqual(r.status_code, 200) self.assertContains(r, draft.title) # find by group - r = self.client.get(base_url + "?activedrafts=on&by=group&group=%s" % draft.group.acronym) + r = self.client.get( + base_url + "?activedrafts=on&by=group&group=%s" % draft.group.acronym + ) self.assertEqual(r.status_code, 200) self.assertContains(r, draft.title) - r = self.client.get(base_url + "?activedrafts=on&by=group&group=%s" % draft.group.acronym.swapcase()) + r = self.client.get( + base_url + + "?activedrafts=on&by=group&group=%s" % draft.group.acronym.swapcase() + ) self.assertEqual(r.status_code, 200) self.assertContains(r, draft.title) # find by area - r = self.client.get(base_url + "?activedrafts=on&by=area&area=%s" % draft.group.parent_id) + r = self.client.get( + base_url + "?activedrafts=on&by=area&area=%s" % draft.group.parent_id + ) self.assertEqual(r.status_code, 200) self.assertContains(r, draft.title) # find by area - r = self.client.get(base_url + "?activedrafts=on&by=area&area=%s" % draft.group.parent_id) + r = self.client.get( + base_url + "?activedrafts=on&by=area&area=%s" % draft.group.parent_id + ) self.assertEqual(r.status_code, 200) self.assertContains(r, draft.title) @@ -164,7 +224,11 @@ def test_search(self): self.assertContains(r, draft.title) # find by IESG state - r = self.client.get(base_url + "?activedrafts=on&by=state&state=%s&substate=" % draft.get_state("draft-iesg").pk) + r = self.client.get( + base_url + + "?activedrafts=on&by=state&state=%s&substate=" + % draft.get_state("draft-iesg").pk + ) self.assertEqual(r.status_code, 200) self.assertContains(r, draft.title) @@ -173,7 +237,7 @@ def test_search_became_rfc(self): rfc = WgRfcFactory() draft.set_state(State.objects.get(type="draft", slug="rfc")) draft.relateddocument_set.create(relationship_id="became_rfc", target=rfc) - base_url = urlreverse('ietf.doc.views_search.search') + base_url = urlreverse("ietf.doc.views_search.search") # find by RFC r = self.client.get(base_url + f"?rfcs=on&name={rfc.name}") @@ -186,123 +250,304 @@ def test_search_became_rfc(self): self.assertContains(r, rfc.title) def test_search_for_name(self): - draft = WgDraftFactory(name='draft-ietf-mars-test',group=GroupFactory(acronym='mars',parent=Group.objects.get(acronym='farfut')),authors=[PersonFactory()],ad=PersonFactory()) + draft = WgDraftFactory( + name="draft-ietf-mars-test", + group=GroupFactory( + acronym="mars", parent=Group.objects.get(acronym="farfut") + ), + authors=[PersonFactory()], + ad=PersonFactory(), + ) draft.set_state(State.objects.get(used=True, type="draft-iesg", slug="pub-req")) - CharterFactory(group=draft.group,name='charter-ietf-mars') - DocumentFactory(type_id='conflrev',name='conflict-review-imaginary-irtf-submission') - DocumentFactory(type_id='statchg',name='status-change-imaginary-mid-review') - DocumentFactory(type_id='agenda',name='agenda-72-mars') - DocumentFactory(type_id='minutes',name='minutes-72-mars') - DocumentFactory(type_id='slides',name='slides-72-mars') + CharterFactory(group=draft.group, name="charter-ietf-mars") + DocumentFactory( + type_id="conflrev", name="conflict-review-imaginary-irtf-submission" + ) + DocumentFactory(type_id="statchg", name="status-change-imaginary-mid-review") + DocumentFactory(type_id="agenda", name="agenda-72-mars") + DocumentFactory(type_id="minutes", name="minutes-72-mars") + DocumentFactory(type_id="slides", name="slides-72-mars") - draft.save_with_history([DocEvent.objects.create(doc=draft, rev=draft.rev, type="changed_document", by=Person.objects.get(user__username="secretary"), desc="Test")]) + draft.save_with_history( + [ + DocEvent.objects.create( + doc=draft, + rev=draft.rev, + type="changed_document", + by=Person.objects.get(user__username="secretary"), + desc="Test", + ) + ] + ) prev_rev = draft.rev draft.rev = "%02d" % (int(prev_rev) + 1) - draft.save_with_history([DocEvent.objects.create(doc=draft, rev=draft.rev, type="changed_document", by=Person.objects.get(user__username="secretary"), desc="Test")]) + draft.save_with_history( + [ + DocEvent.objects.create( + doc=draft, + rev=draft.rev, + type="changed_document", + by=Person.objects.get(user__username="secretary"), + desc="Test", + ) + ] + ) # exact match - r = self.client.get(urlreverse('ietf.doc.views_search.search_for_name', kwargs=dict(name=draft.name))) + r = self.client.get( + urlreverse( + "ietf.doc.views_search.search_for_name", kwargs=dict(name=draft.name) + ) + ) self.assertEqual(r.status_code, 302) - self.assertEqual(urlparse(r["Location"]).path, urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) + self.assertEqual( + urlparse(r["Location"]).path, + urlreverse( + "ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name) + ), + ) # mixed-up case exact match - r = self.client.get(urlreverse('ietf.doc.views_search.search_for_name', kwargs=dict(name=draft.name.swapcase()))) + r = self.client.get( + urlreverse( + "ietf.doc.views_search.search_for_name", + kwargs=dict(name=draft.name.swapcase()), + ) + ) self.assertEqual(r.status_code, 302) - self.assertEqual(urlparse(r["Location"]).path, urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) + self.assertEqual( + urlparse(r["Location"]).path, + urlreverse( + "ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name) + ), + ) # prefix match - r = self.client.get(urlreverse('ietf.doc.views_search.search_for_name', kwargs=dict(name="-".join(draft.name.split("-")[:-1])))) + r = self.client.get( + urlreverse( + "ietf.doc.views_search.search_for_name", + kwargs=dict(name="-".join(draft.name.split("-")[:-1])), + ) + ) self.assertEqual(r.status_code, 302) - self.assertEqual(urlparse(r["Location"]).path, urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) + self.assertEqual( + urlparse(r["Location"]).path, + urlreverse( + "ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name) + ), + ) # mixed-up case prefix match r = self.client.get( urlreverse( - 'ietf.doc.views_search.search_for_name', + "ietf.doc.views_search.search_for_name", kwargs=dict(name="-".join(draft.name.swapcase().split("-")[:-1])), - )) + ) + ) self.assertEqual(r.status_code, 302) - self.assertEqual(urlparse(r["Location"]).path, urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) + self.assertEqual( + urlparse(r["Location"]).path, + urlreverse( + "ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name) + ), + ) # non-prefix match - r = self.client.get(urlreverse('ietf.doc.views_search.search_for_name', kwargs=dict(name="-".join(draft.name.split("-")[1:])))) + r = self.client.get( + urlreverse( + "ietf.doc.views_search.search_for_name", + kwargs=dict(name="-".join(draft.name.split("-")[1:])), + ) + ) self.assertEqual(r.status_code, 302) - self.assertEqual(urlparse(r["Location"]).path, urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) + self.assertEqual( + urlparse(r["Location"]).path, + urlreverse( + "ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name) + ), + ) # mixed-up case non-prefix match r = self.client.get( urlreverse( - 'ietf.doc.views_search.search_for_name', + "ietf.doc.views_search.search_for_name", kwargs=dict(name="-".join(draft.name.swapcase().split("-")[1:])), - )) + ) + ) self.assertEqual(r.status_code, 302) - self.assertEqual(urlparse(r["Location"]).path, urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) + self.assertEqual( + urlparse(r["Location"]).path, + urlreverse( + "ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name) + ), + ) # other doctypes than drafts - doc = Document.objects.get(name='charter-ietf-mars') - r = self.client.get(urlreverse('ietf.doc.views_search.search_for_name', kwargs=dict(name='charter-ietf-ma'))) + doc = Document.objects.get(name="charter-ietf-mars") + r = self.client.get( + urlreverse( + "ietf.doc.views_search.search_for_name", + kwargs=dict(name="charter-ietf-ma"), + ) + ) self.assertEqual(r.status_code, 302) - self.assertEqual(urlparse(r["Location"]).path, urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name))) + self.assertEqual( + urlparse(r["Location"]).path, + urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name)), + ) - doc = Document.objects.filter(name__startswith='conflict-review-').first() - r = self.client.get(urlreverse('ietf.doc.views_search.search_for_name', kwargs=dict(name="-".join(doc.name.split("-")[:-1])))) + doc = Document.objects.filter(name__startswith="conflict-review-").first() + r = self.client.get( + urlreverse( + "ietf.doc.views_search.search_for_name", + kwargs=dict(name="-".join(doc.name.split("-")[:-1])), + ) + ) self.assertEqual(r.status_code, 302) - self.assertEqual(urlparse(r["Location"]).path, urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name))) + self.assertEqual( + urlparse(r["Location"]).path, + urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name)), + ) - doc = Document.objects.filter(name__startswith='status-change-').first() - r = self.client.get(urlreverse('ietf.doc.views_search.search_for_name', kwargs=dict(name="-".join(doc.name.split("-")[:-1])))) + doc = Document.objects.filter(name__startswith="status-change-").first() + r = self.client.get( + urlreverse( + "ietf.doc.views_search.search_for_name", + kwargs=dict(name="-".join(doc.name.split("-")[:-1])), + ) + ) self.assertEqual(r.status_code, 302) - self.assertEqual(urlparse(r["Location"]).path, urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name))) + self.assertEqual( + urlparse(r["Location"]).path, + urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name)), + ) - doc = Document.objects.filter(name__startswith='agenda-').first() - r = self.client.get(urlreverse('ietf.doc.views_search.search_for_name', kwargs=dict(name="-".join(doc.name.split("-")[:-1])))) + doc = Document.objects.filter(name__startswith="agenda-").first() + r = self.client.get( + urlreverse( + "ietf.doc.views_search.search_for_name", + kwargs=dict(name="-".join(doc.name.split("-")[:-1])), + ) + ) self.assertEqual(r.status_code, 302) - self.assertEqual(urlparse(r["Location"]).path, urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name))) + self.assertEqual( + urlparse(r["Location"]).path, + urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name)), + ) - doc = Document.objects.filter(name__startswith='minutes-').first() - r = self.client.get(urlreverse('ietf.doc.views_search.search_for_name', kwargs=dict(name="-".join(doc.name.split("-")[:-1])))) + doc = Document.objects.filter(name__startswith="minutes-").first() + r = self.client.get( + urlreverse( + "ietf.doc.views_search.search_for_name", + kwargs=dict(name="-".join(doc.name.split("-")[:-1])), + ) + ) self.assertEqual(r.status_code, 302) - self.assertEqual(urlparse(r["Location"]).path, urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name))) + self.assertEqual( + urlparse(r["Location"]).path, + urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name)), + ) - doc = Document.objects.filter(name__startswith='slides-').first() - r = self.client.get(urlreverse('ietf.doc.views_search.search_for_name', kwargs=dict(name="-".join(doc.name.split("-")[:-1])))) + doc = Document.objects.filter(name__startswith="slides-").first() + r = self.client.get( + urlreverse( + "ietf.doc.views_search.search_for_name", + kwargs=dict(name="-".join(doc.name.split("-")[:-1])), + ) + ) self.assertEqual(r.status_code, 302) - self.assertEqual(urlparse(r["Location"]).path, urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name))) + self.assertEqual( + urlparse(r["Location"]).path, + urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name)), + ) # match with revision - r = self.client.get(urlreverse('ietf.doc.views_search.search_for_name', kwargs=dict(name=draft.name + "-" + prev_rev))) + r = self.client.get( + urlreverse( + "ietf.doc.views_search.search_for_name", + kwargs=dict(name=draft.name + "-" + prev_rev), + ) + ) self.assertEqual(r.status_code, 302) - self.assertEqual(urlparse(r["Location"]).path, urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name, rev=prev_rev))) + self.assertEqual( + urlparse(r["Location"]).path, + urlreverse( + "ietf.doc.views_doc.document_main", + kwargs=dict(name=draft.name, rev=prev_rev), + ), + ) # match with non-existing revision - r = self.client.get(urlreverse('ietf.doc.views_search.search_for_name', kwargs=dict(name=draft.name + "-09"))) + r = self.client.get( + urlreverse( + "ietf.doc.views_search.search_for_name", + kwargs=dict(name=draft.name + "-09"), + ) + ) self.assertEqual(r.status_code, 302) - self.assertEqual(urlparse(r["Location"]).path, urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) + self.assertEqual( + urlparse(r["Location"]).path, + urlreverse( + "ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name) + ), + ) # match with revision and extension - r = self.client.get(urlreverse('ietf.doc.views_search.search_for_name', kwargs=dict(name=draft.name + "-" + prev_rev + ".txt"))) + r = self.client.get( + urlreverse( + "ietf.doc.views_search.search_for_name", + kwargs=dict(name=draft.name + "-" + prev_rev + ".txt"), + ) + ) self.assertEqual(r.status_code, 302) - self.assertEqual(urlparse(r["Location"]).path, urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name, rev=prev_rev))) - + self.assertEqual( + urlparse(r["Location"]).path, + urlreverse( + "ietf.doc.views_doc.document_main", + kwargs=dict(name=draft.name, rev=prev_rev), + ), + ) + # no match - r = self.client.get(urlreverse('ietf.doc.views_search.search_for_name', kwargs=dict(name="draft-ietf-doesnotexist-42"))) + r = self.client.get( + urlreverse( + "ietf.doc.views_search.search_for_name", + kwargs=dict(name="draft-ietf-doesnotexist-42"), + ) + ) self.assertEqual(r.status_code, 302) parsed = urlparse(r["Location"]) - self.assertEqual(parsed.path, urlreverse('ietf.doc.views_search.search')) - self.assertEqual(parse_qs(parsed.query)["name"][0], "draft-ietf-doesnotexist-42") - + self.assertEqual(parsed.path, urlreverse("ietf.doc.views_search.search")) + self.assertEqual( + parse_qs(parsed.query)["name"][0], "draft-ietf-doesnotexist-42" + ) + def test_search_rfc(self): rfc = WgRfcFactory(name="rfc0000") - + # search for existing RFC should redirect directly to the RFC page - r = self.client.get(urlreverse('ietf.doc.views_search.search_for_name', kwargs=dict(name=rfc.name))) - self.assertRedirects(r, f'/doc/{rfc.name}/', status_code=302, target_status_code=200) + r = self.client.get( + urlreverse( + "ietf.doc.views_search.search_for_name", kwargs=dict(name=rfc.name) + ) + ) + self.assertRedirects( + r, f"/doc/{rfc.name}/", status_code=302, target_status_code=200 + ) # search for existing RFC with revision number should redirect to the RFC page - r = self.client.get(urlreverse('ietf.doc.views_search.search_for_name', kwargs=dict(name=rfc.name + "-99")), follow=True) - self.assertRedirects(r, f'/doc/{rfc.name}/', status_code=302, target_status_code=200) + r = self.client.get( + urlreverse( + "ietf.doc.views_search.search_for_name", + kwargs=dict(name=rfc.name + "-99"), + ), + follow=True, + ) + self.assertRedirects( + r, f"/doc/{rfc.name}/", status_code=302, target_status_code=200 + ) def test_frontpage(self): r = self.client.get("/") @@ -367,31 +612,48 @@ def test_ad_workload(self): ) def test_docs_for_ad(self): - ad = RoleFactory(name_id='ad',group__type_id='area',group__state_id='active').person + ad = RoleFactory( + name_id="ad", group__type_id="area", group__state_id="active" + ).person draft = IndividualDraftFactory(ad=ad) draft.action_holders.set([PersonFactory()]) - draft.set_state(State.objects.get(type='draft-iesg', slug='lc')) + draft.set_state(State.objects.get(type="draft-iesg", slug="lc")) rfc = IndividualRfcFactory(ad=ad) - conflrev = DocumentFactory(type_id='conflrev',ad=ad) - conflrev.set_state(State.objects.get(type='conflrev', slug='iesgeval')) - statchg = DocumentFactory(type_id='statchg',ad=ad) - statchg.set_state(State.objects.get(type='statchg', slug='iesgeval')) - charter = CharterFactory(name='charter-ietf-ames',ad=ad) - charter.set_state(State.objects.get(type='charter', slug='iesgrev')) - - ballot_type = BallotType.objects.get(doc_type_id='draft',slug='approve') - ballot = BallotDocEventFactory(ballot_type=ballot_type, doc__states=[('draft-iesg','iesg-eva')]) - discuss_pos = BallotPositionName.objects.get(slug='discuss') - discuss_other = BallotPositionDocEventFactory(ballot=ballot, doc=ballot.doc, balloter=ad, pos=discuss_pos) - - blockedcharter = CharterFactory(name='charter-ietf-mars',ad=ad) - blockedcharter.set_state(State.objects.get(type='charter',slug='extrev')) - charter_ballot_type = BallotType.objects.get(doc_type_id='charter',slug='approve') - charterballot = BallotDocEventFactory(ballot_type=charter_ballot_type, doc__states=[('charter','extrev')]) - block_pos = BallotPositionName.objects.get(slug='block') - block_other = BallotPositionDocEventFactory(ballot=charterballot, doc=ballot.doc, balloter=ad, pos=block_pos) - - r = self.client.get(urlreverse('ietf.doc.views_search.docs_for_ad', kwargs=dict(name=ad.full_name_as_key()))) + conflrev = DocumentFactory(type_id="conflrev", ad=ad) + conflrev.set_state(State.objects.get(type="conflrev", slug="iesgeval")) + statchg = DocumentFactory(type_id="statchg", ad=ad) + statchg.set_state(State.objects.get(type="statchg", slug="iesgeval")) + charter = CharterFactory(name="charter-ietf-ames", ad=ad) + charter.set_state(State.objects.get(type="charter", slug="iesgrev")) + + ballot_type = BallotType.objects.get(doc_type_id="draft", slug="approve") + ballot = BallotDocEventFactory( + ballot_type=ballot_type, doc__states=[("draft-iesg", "iesg-eva")] + ) + discuss_pos = BallotPositionName.objects.get(slug="discuss") + discuss_other = BallotPositionDocEventFactory( + ballot=ballot, doc=ballot.doc, balloter=ad, pos=discuss_pos + ) + + blockedcharter = CharterFactory(name="charter-ietf-mars", ad=ad) + blockedcharter.set_state(State.objects.get(type="charter", slug="extrev")) + charter_ballot_type = BallotType.objects.get( + doc_type_id="charter", slug="approve" + ) + charterballot = BallotDocEventFactory( + ballot_type=charter_ballot_type, doc__states=[("charter", "extrev")] + ) + block_pos = BallotPositionName.objects.get(slug="block") + block_other = BallotPositionDocEventFactory( + ballot=charterballot, doc=ballot.doc, balloter=ad, pos=block_pos + ) + + r = self.client.get( + urlreverse( + "ietf.doc.views_search.docs_for_ad", + kwargs=dict(name=ad.full_name_as_key()), + ) + ) self.assertEqual(r.status_code, 200) self.assertContains(r, draft.name) self.assertContains(r, escape(draft.action_holders.first().name)) @@ -405,22 +667,34 @@ def test_docs_for_ad(self): def test_auth48_doc_for_ad(self): """Docs in AUTH48 state should have a decoration""" - ad = RoleFactory(name_id='ad', group__type_id='area', group__state_id='active').person - draft = IndividualDraftFactory(ad=ad, - states=[('draft', 'active'), - ('draft-iesg', 'rfcqueue'), - ('draft-rfceditor', 'auth48')]) - r = self.client.get(urlreverse('ietf.doc.views_search.docs_for_ad', - kwargs=dict(name=ad.full_name_as_key()))) + ad = RoleFactory( + name_id="ad", group__type_id="area", group__state_id="active" + ).person + draft = IndividualDraftFactory( + ad=ad, + states=[ + ("draft", "active"), + ("draft-iesg", "rfcqueue"), + ("draft-rfceditor", "auth48"), + ], + ) + r = self.client.get( + urlreverse( + "ietf.doc.views_search.docs_for_ad", + kwargs=dict(name=ad.full_name_as_key()), + ) + ) self.assertEqual(r.status_code, 200) self.assertContains(r, draft.name) - self.assertContains(r, 'title="AUTH48"') # title attribute of AUTH48 badge in auth48_alert_badge filter + self.assertContains( + r, 'title="AUTH48"' + ) # title attribute of AUTH48 badge in auth48_alert_badge filter def test_drafts_in_last_call(self): draft = IndividualDraftFactory(pages=1) draft.action_holders.set([PersonFactory()]) draft.set_state(State.objects.get(type="draft-iesg", slug="lc")) - r = self.client.get(urlreverse('ietf.doc.views_search.drafts_in_last_call')) + r = self.client.get(urlreverse("ietf.doc.views_search.drafts_in_last_call")) self.assertEqual(r.status_code, 200) self.assertContains(r, draft.title) self.assertContains(r, escape(draft.action_holders.first().name)) @@ -428,24 +702,24 @@ def test_drafts_in_last_call(self): def test_in_iesg_process(self): doc_in_process = IndividualDraftFactory() doc_in_process.action_holders.set([PersonFactory()]) - doc_in_process.set_state(State.objects.get(type='draft-iesg', slug='lc')) + doc_in_process.set_state(State.objects.get(type="draft-iesg", slug="lc")) doc_not_in_process = IndividualDraftFactory() - r = self.client.get(urlreverse('ietf.doc.views_search.drafts_in_iesg_process')) + r = self.client.get(urlreverse("ietf.doc.views_search.drafts_in_iesg_process")) self.assertEqual(r.status_code, 200) self.assertContains(r, doc_in_process.title) self.assertContains(r, escape(doc_in_process.action_holders.first().name)) self.assertNotContains(r, doc_not_in_process.title) - + def test_indexes(self): draft = IndividualDraftFactory() rfc = WgRfcFactory() - r = self.client.get(urlreverse('ietf.doc.views_search.index_all_drafts')) + r = self.client.get(urlreverse("ietf.doc.views_search.index_all_drafts")) self.assertEqual(r.status_code, 200) self.assertContains(r, draft.name) self.assertContains(r, rfc.name.upper()) - r = self.client.get(urlreverse('ietf.doc.views_search.index_active_drafts')) + r = self.client.get(urlreverse("ietf.doc.views_search.index_active_drafts")) self.assertEqual(r.status_code, 200) self.assertContains(r, draft.title) @@ -454,28 +728,37 @@ def test_ajax_search_docs(self): rfc = IndividualRfcFactory(rfc_number=1234) bcp = IndividualRfcFactory(name="bcp12345", type_id="bcp") - url = urlreverse('ietf.doc.views_search.ajax_select2_search_docs', kwargs={ - "model_name": "document", - "doc_type": "draft", - }) + url = urlreverse( + "ietf.doc.views_search.ajax_select2_search_docs", + kwargs={ + "model_name": "document", + "doc_type": "draft", + }, + ) r = self.client.get(url, dict(q=draft.name)) self.assertEqual(r.status_code, 200) data = r.json() self.assertEqual(data[0]["id"], draft.pk) - url = urlreverse('ietf.doc.views_search.ajax_select2_search_docs', kwargs={ - "model_name": "document", - "doc_type": "rfc", - }) + url = urlreverse( + "ietf.doc.views_search.ajax_select2_search_docs", + kwargs={ + "model_name": "document", + "doc_type": "rfc", + }, + ) r = self.client.get(url, dict(q=rfc.name)) self.assertEqual(r.status_code, 200) data = r.json() self.assertEqual(data[0]["id"], rfc.pk) - url = urlreverse('ietf.doc.views_search.ajax_select2_search_docs', kwargs={ - "model_name": "document", - "doc_type": "all", - }) + url = urlreverse( + "ietf.doc.views_search.ajax_select2_search_docs", + kwargs={ + "model_name": "document", + "doc_type": "all", + }, + ) r = self.client.get(url, dict(q="1234")) self.assertEqual(r.status_code, 200) data = r.json() @@ -483,30 +766,39 @@ def test_ajax_search_docs(self): pks = set([data[i]["id"] for i in range(3)]) self.assertEqual(pks, set([bcp.pk, rfc.pk, draft.pk])) - - def test_recent_drafts(self): # Three drafts to show with various warnings - drafts = WgDraftFactory.create_batch(3,states=[('draft','active'),('draft-iesg','ad-eval')]) + drafts = WgDraftFactory.create_batch( + 3, states=[("draft", "active"), ("draft-iesg", "ad-eval")] + ) for index, draft in enumerate(drafts): - StateDocEventFactory(doc=draft, state=('draft-iesg','ad-eval'), time=timezone.now()-datetime.timedelta(days=[1,15,29][index])) + StateDocEventFactory( + doc=draft, + state=("draft-iesg", "ad-eval"), + time=timezone.now() - datetime.timedelta(days=[1, 15, 29][index]), + ) draft.action_holders.set([PersonFactory()]) # And one draft that should not show (with the default of 7 days to view) old = WgDraftFactory() - old.docevent_set.filter(newrevisiondocevent__isnull=False).update(time=timezone.now()-datetime.timedelta(days=8)) - StateDocEventFactory(doc=old, time=timezone.now()-datetime.timedelta(days=8)) + old.docevent_set.filter(newrevisiondocevent__isnull=False).update( + time=timezone.now() - datetime.timedelta(days=8) + ) + StateDocEventFactory(doc=old, time=timezone.now() - datetime.timedelta(days=8)) - url = urlreverse('ietf.doc.views_search.recent_drafts') + url = urlreverse("ietf.doc.views_search.recent_drafts") r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertEqual(len(q('td.doc')),3) - self.assertTrue(q('td.status span.text-bg-warning[title*="%s"]' % "for 15 days")) + self.assertEqual(len(q("td.doc")), 3) + self.assertTrue( + q('td.status span.text-bg-warning[title*="%s"]' % "for 15 days") + ) self.assertTrue(q('td.status span.text-bg-danger[title*="%s"]' % "for 29 days")) for ah in [draft.action_holders.first() for draft in drafts]: self.assertContains(r, escape(ah.name)) + class DocDraftTestCase(TestCase): draft_text = """ @@ -680,43 +972,63 @@ class DocDraftTestCase(TestCase): def setUp(self): super().setUp() - for dir in [settings.INTERNET_ALL_DRAFTS_ARCHIVE_DIR, settings.INTERNET_DRAFT_PATH]: - with (Path(dir) / 'draft-ietf-mars-test-01.txt').open('w') as f: + for dir in [ + settings.INTERNET_ALL_DRAFTS_ARCHIVE_DIR, + settings.INTERNET_DRAFT_PATH, + ]: + with (Path(dir) / "draft-ietf-mars-test-01.txt").open("w") as f: f.write(self.draft_text) def test_document_draft(self): - draft = WgDraftFactory(name='draft-ietf-mars-test',rev='01', create_revisions=range(0,2)) + draft = WgDraftFactory( + name="draft-ietf-mars-test", rev="01", create_revisions=range(0, 2) + ) HolderIprDisclosureFactory(docs=[draft]) - + # Docs for testing relationships. Does not test 'possibly-replaces'. The 'replaced_by' direction # is tested separately below. replaced = IndividualDraftFactory() - draft.relateddocument_set.create(relationship_id='replaces',source=draft,target=replaced) + draft.relateddocument_set.create( + relationship_id="replaces", source=draft, target=replaced + ) obsoleted = IndividualDraftFactory() - draft.relateddocument_set.create(relationship_id='obs',source=draft,target=obsoleted) + draft.relateddocument_set.create( + relationship_id="obs", source=draft, target=obsoleted + ) obsoleted_by = IndividualDraftFactory() - obsoleted_by.relateddocument_set.create(relationship_id='obs',source=obsoleted_by,target=draft) + obsoleted_by.relateddocument_set.create( + relationship_id="obs", source=obsoleted_by, target=draft + ) updated = IndividualDraftFactory() - draft.relateddocument_set.create(relationship_id='updates',source=draft,target=updated) + draft.relateddocument_set.create( + relationship_id="updates", source=draft, target=updated + ) updated_by = IndividualDraftFactory() - updated_by.relateddocument_set.create(relationship_id='updates',source=obsoleted_by,target=draft) + updated_by.relateddocument_set.create( + relationship_id="updates", source=obsoleted_by, target=draft + ) DocExtResourceFactory(doc=draft) # these tests aren't testing all attributes yet, feel free to # expand them - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + ) self.assertEqual(r.status_code, 200) self.assertContains(r, "Active Internet-Draft") - if settings.USER_PREFERENCE_DEFAULTS['full_draft'] == 'off': + if settings.USER_PREFERENCE_DEFAULTS["full_draft"] == "off": self.assertContains(r, "Show full document") self.assertNotContains(r, "Deimos street") self.assertContains(r, replaced.name) self.assertContains(r, replaced.title) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + "?include_text=0") + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + + "?include_text=0" + ) self.assertEqual(r.status_code, 200) self.assertContains(r, "Active Internet-Draft") self.assertContains(r, "Show full document") @@ -724,7 +1036,10 @@ def test_document_draft(self): self.assertContains(r, replaced.name) self.assertContains(r, replaced.title) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + "?include_text=foo") + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + + "?include_text=foo" + ) self.assertEqual(r.status_code, 200) self.assertContains(r, "Active Internet-Draft") self.assertNotContains(r, "Show full document") @@ -732,7 +1047,10 @@ def test_document_draft(self): self.assertContains(r, replaced.name) self.assertContains(r, replaced.title) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + "?include_text=1") + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + + "?include_text=1" + ) self.assertEqual(r.status_code, 200) self.assertContains(r, "Active Internet-Draft") self.assertNotContains(r, "Show full document") @@ -740,8 +1058,10 @@ def test_document_draft(self): self.assertContains(r, replaced.name) self.assertContains(r, replaced.title) - self.client.cookies = SimpleCookie({str('full_draft'): str('on')}) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) + self.client.cookies = SimpleCookie({str("full_draft"): str("on")}) + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + ) self.assertEqual(r.status_code, 200) self.assertContains(r, "Active Internet-Draft") self.assertNotContains(r, "Show full document") @@ -749,8 +1069,10 @@ def test_document_draft(self): self.assertContains(r, replaced.name) self.assertContains(r, replaced.title) - self.client.cookies = SimpleCookie({str('full_draft'): str('off')}) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) + self.client.cookies = SimpleCookie({str("full_draft"): str("off")}) + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + ) self.assertEqual(r.status_code, 200) self.assertContains(r, "Active Internet-Draft") self.assertContains(r, "Show full document") @@ -758,55 +1080,97 @@ def test_document_draft(self): self.assertContains(r, replaced.name) self.assertContains(r, replaced.title) - self.client.cookies = SimpleCookie({str('full_draft'): str('foo')}) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) + self.client.cookies = SimpleCookie({str("full_draft"): str("foo")}) + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + ) self.assertEqual(r.status_code, 200) self.assertContains(r, "Active Internet-Draft") - if settings.USER_PREFERENCE_DEFAULTS['full_draft'] == 'off': + if settings.USER_PREFERENCE_DEFAULTS["full_draft"] == "off": self.assertContains(r, "Show full document") self.assertNotContains(r, "Deimos street") self.assertContains(r, replaced.name) self.assertContains(r, replaced.title) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_html", kwargs=dict(name=draft.name))) + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_html", kwargs=dict(name=draft.name)) + ) self.assertEqual(r.status_code, 200) self.assertContains(r, "Select version") self.assertContains(r, "Deimos street") q = PyQuery(r.content) - self.assertEqual(q('title').text(), 'draft-ietf-mars-test-01') - self.assertEqual(len(q('.rfcmarkup pre')), 3) - self.assertEqual(len(q('.rfcmarkup span.h1, .rfcmarkup h1')), 2) - self.assertEqual(len(q('.rfcmarkup a[href]')), 27) + self.assertEqual(q("title").text(), "draft-ietf-mars-test-01") + self.assertEqual(len(q(".rfcmarkup pre")), 3) + self.assertEqual(len(q(".rfcmarkup span.h1, .rfcmarkup h1")), 2) + self.assertEqual(len(q(".rfcmarkup a[href]")), 27) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_html", kwargs=dict(name=draft.name, rev=draft.rev))) + r = self.client.get( + urlreverse( + "ietf.doc.views_doc.document_html", + kwargs=dict(name=draft.name, rev=draft.rev), + ) + ) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertEqual(q('title').text(), 'draft-ietf-mars-test-01') + self.assertEqual(q("title").text(), "draft-ietf-mars-test-01") # check that revision list has expected versions - self.assertEqual(len(q('#sidebar .revision-list .page-item.active a.page-link[href$="draft-ietf-mars-test-01"]')), 1) + self.assertEqual( + len( + q( + '#sidebar .revision-list .page-item.active a.page-link[href$="draft-ietf-mars-test-01"]' + ) + ), + 1, + ) # check that diff dropdowns have expected versions - self.assertEqual(len(q('#sidebar option[value="draft-ietf-mars-test-00"][selected="selected"]')), 1) + self.assertEqual( + len( + q( + '#sidebar option[value="draft-ietf-mars-test-00"][selected="selected"]' + ) + ), + 1, + ) rfc = WgRfcFactory() rfc.save_with_history([DocEventFactory(doc=rfc)]) (Path(settings.RFC_PATH) / rfc.get_base_name()).touch() - r = self.client.get(urlreverse("ietf.doc.views_doc.document_html", kwargs=dict(name=rfc.name))) + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_html", kwargs=dict(name=rfc.name)) + ) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertEqual(q('title').text(), f'RFC {rfc.rfc_number} - {rfc.title}') + self.assertEqual(q("title").text(), f"RFC {rfc.rfc_number} - {rfc.title}") # synonyms for the rfc should be redirected to its canonical view - r = self.client.get(urlreverse("ietf.doc.views_doc.document_html", kwargs=dict(name=rfc.rfc_number))) - self.assertRedirects(r, urlreverse("ietf.doc.views_doc.document_html", kwargs=dict(name=rfc.name))) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_html", kwargs=dict(name=f'RFC {rfc.rfc_number}'))) - self.assertRedirects(r, urlreverse("ietf.doc.views_doc.document_html", kwargs=dict(name=rfc.name))) + r = self.client.get( + urlreverse( + "ietf.doc.views_doc.document_html", kwargs=dict(name=rfc.rfc_number) + ) + ) + self.assertRedirects( + r, + urlreverse("ietf.doc.views_doc.document_html", kwargs=dict(name=rfc.name)), + ) + r = self.client.get( + urlreverse( + "ietf.doc.views_doc.document_html", + kwargs=dict(name=f"RFC {rfc.rfc_number}"), + ) + ) + self.assertRedirects( + r, + urlreverse("ietf.doc.views_doc.document_html", kwargs=dict(name=rfc.name)), + ) # expired draft draft.set_state(State.objects.get(type="draft", slug="expired")) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + ) self.assertEqual(r.status_code, 200) self.assertContains(r, "Expired Internet-Draft") @@ -817,15 +1181,25 @@ def test_document_draft(self): name="draft-ietf-replacement", time=timezone.now(), title="Replacement Draft", - stream_id=draft.stream_id, group_id=draft.group_id, abstract=draft.abstract,stream=draft.stream, rev=draft.rev, - pages=draft.pages, intended_std_level_id=draft.intended_std_level_id, - shepherd_id=draft.shepherd_id, ad_id=draft.ad_id, expires=draft.expires, - notify=draft.notify) - rel = RelatedDocument.objects.create(source=replacement, - target=draft, - relationship_id="replaces") + stream_id=draft.stream_id, + group_id=draft.group_id, + abstract=draft.abstract, + stream=draft.stream, + rev=draft.rev, + pages=draft.pages, + intended_std_level_id=draft.intended_std_level_id, + shepherd_id=draft.shepherd_id, + ad_id=draft.ad_id, + expires=draft.expires, + notify=draft.notify, + ) + rel = RelatedDocument.objects.create( + source=replacement, target=draft, relationship_id="replaces" + ) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + ) self.assertEqual(r.status_code, 200) self.assertContains(r, "Replaced Internet-Draft") self.assertContains(r, replacement.name) @@ -837,27 +1211,48 @@ def test_document_draft(self): draft.std_level_id = "ps" rfc = WgRfcFactory(group=draft.group, name="rfc123456") - rfc.save_with_history([DocEvent.objects.create(doc=rfc, rev=None, type="published_rfc", by=Person.objects.get(name="(System)"))]) + rfc.save_with_history( + [ + DocEvent.objects.create( + doc=rfc, + rev=None, + type="published_rfc", + by=Person.objects.get(name="(System)"), + ) + ] + ) draft.relateddocument_set.create(relationship_id="became_rfc", target=rfc) obsoleted = IndividualRfcFactory() - rfc.relateddocument_set.create(relationship_id='obs',target=obsoleted) + rfc.relateddocument_set.create(relationship_id="obs", target=obsoleted) obsoleted_by = IndividualRfcFactory() - obsoleted_by.relateddocument_set.create(relationship_id='obs',target=rfc) + obsoleted_by.relateddocument_set.create(relationship_id="obs", target=rfc) updated = IndividualRfcFactory() - rfc.relateddocument_set.create(relationship_id='updates',target=updated) + rfc.relateddocument_set.create(relationship_id="updates", target=updated) updated_by = IndividualRfcFactory() - updated_by.relateddocument_set.create(relationship_id='updates',target=rfc) + updated_by.relateddocument_set.create(relationship_id="updates", target=rfc) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name, rev=draft.rev))) + r = self.client.get( + urlreverse( + "ietf.doc.views_doc.document_main", + kwargs=dict(name=draft.name, rev=draft.rev), + ) + ) self.assertEqual(r.status_code, 200) - self.assertContains(r, "This is an older version of an Internet-Draft that was ultimately published as") + self.assertContains( + r, + "This is an older version of an Internet-Draft that was ultimately published as", + ) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + ) self.assertEqual(r.status_code, 302) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=rfc.name))) + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=rfc.name)) + ) self.assertEqual(r.status_code, 200) self.assertContains(r, "RFC 123456") self.assertContains(r, draft.name) @@ -876,51 +1271,60 @@ def test_document_draft(self): name="rfc1234567", title="RFC without a Draft", stream_id="ise", - std_level_id="ps") - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=rfc.name))) + std_level_id="ps", + ) + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=rfc.name)) + ) self.assertEqual(r.status_code, 200) self.assertContains(r, "RFC 1234567") # unknown draft - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name="draft-xyz123"))) + r = self.client.get( + urlreverse( + "ietf.doc.views_doc.document_main", kwargs=dict(name="draft-xyz123") + ) + ) self.assertEqual(r.status_code, 404) def assert_correct_wg_group_link(self, r, group): """Assert correct format for WG-like group types""" self.assertContains( r, - '(%(group_acro)s %(group_type)s)' % { + '(%(group_acro)s %(group_type)s)' + % { "group_acro": group.acronym, "group_type": group.type, "about_url": group.about_url(), }, - msg_prefix='WG-like group %s (%s) should include group type in link' % (group.acronym, group.type), + msg_prefix="WG-like group %s (%s) should include group type in link" + % (group.acronym, group.type), ) def test_draft_status_changes(self): draft = WgRfcFactory() status_change_doc = StatusChangeFactory( group=draft.group, - changes_status_of=[('tops', draft)], + changes_status_of=[("tops", draft)], ) status_change_url = urlreverse( - 'ietf.doc.views_doc.document_main', - kwargs={'name': status_change_doc.name}, + "ietf.doc.views_doc.document_main", + kwargs={"name": status_change_doc.name}, ) proposed_status_change_doc = StatusChangeFactory( group=draft.group, - changes_status_of=[('tobcp', draft)], - states=[State.objects.get(slug='needshep', type='statchg')], + changes_status_of=[("tobcp", draft)], + states=[State.objects.get(slug="needshep", type="statchg")], ) proposed_status_change_url = urlreverse( - 'ietf.doc.views_doc.document_main', - kwargs={'name': proposed_status_change_doc.name}, + "ietf.doc.views_doc.document_main", + kwargs={"name": proposed_status_change_doc.name}, ) r = self.client.get( urlreverse( - 'ietf.doc.views_doc.document_main', - kwargs={'name': draft.name}, + "ietf.doc.views_doc.document_main", + kwargs={"name": draft.name}, ) ) self.assertEqual(r.status_code, 200) @@ -946,36 +1350,44 @@ def assert_correct_non_wg_group_link(self, r, group): """Assert correct format for non-WG-like group types""" self.assertContains( r, - '(%(group_acro)s)' % { + '(%(group_acro)s)' + % { "group_acro": group.acronym, "about_url": group.about_url(), }, - msg_prefix='Non-WG-like group %s (%s) should not include group type in link' % (group.acronym, group.type), + msg_prefix="Non-WG-like group %s (%s) should not include group type in link" + % (group.acronym, group.type), ) def login(self, username): - self.client.login(username=username, password=username + '+password') + self.client.login(username=username, password=username + "+password") def test_edit_authors_permissions(self): """Only the secretariat may edit authors""" draft = WgDraftFactory(authors=PersonFactory.create_batch(3)) - RoleFactory(group=draft.group, name_id='chair') - RoleFactory(group=draft.group, name_id='ad', person=Person.objects.get(user__username='ad')) - url = urlreverse('ietf.doc.views_doc.edit_authors', kwargs=dict(name=draft.name)) + RoleFactory(group=draft.group, name_id="chair") + RoleFactory( + group=draft.group, + name_id="ad", + person=Person.objects.get(user__username="ad"), + ) + url = urlreverse( + "ietf.doc.views_doc.edit_authors", kwargs=dict(name=draft.name) + ) # Relevant users not authorized to edit authors unauthorized_usernames = [ - 'plain', + "plain", *[author.user.username for author in draft.authors()], draft.group.get_chair().person.user.username, - 'ad' + "ad", ] # First, check that only the secretary can even see the edit page. # Each call checks that currently-logged in user is refused, then logs in as the named user. for username in unauthorized_usernames: login_testing_unauthorized(self, username, url) - login_testing_unauthorized(self, 'secretary', url) + login_testing_unauthorized(self, "secretary", url) r = self.client.get(url) self.assertEqual(r.status_code, 200) self.client.logout() @@ -983,7 +1395,7 @@ def test_edit_authors_permissions(self): # Try to add an author via POST - still only the secretary should be able to do this. orig_authors = draft.authors() post_data = self.make_edit_authors_post_data( - basis='permission test', + basis="permission test", authors=draft.documentauthor_set.all(), ) new_auth_person = PersonFactory() @@ -992,15 +1404,21 @@ def test_edit_authors_permissions(self): dict( person=str(new_auth_person.pk), email=str(new_auth_person.email()), - affiliation='affil', - country='USA', + affiliation="affil", + country="USA", ), ) for username in unauthorized_usernames: - login_testing_unauthorized(self, username, url, method='post', request_kwargs=dict(data=post_data)) + login_testing_unauthorized( + self, username, url, method="post", request_kwargs=dict(data=post_data) + ) draft = Document.objects.get(pk=draft.pk) - self.assertEqual(draft.authors(), orig_authors) # ensure draft author list was not modified - login_testing_unauthorized(self, 'secretary', url, method='post', request_kwargs=dict(data=post_data)) + self.assertEqual( + draft.authors(), orig_authors + ) # ensure draft author list was not modified + login_testing_unauthorized( + self, "secretary", url, method="post", request_kwargs=dict(data=post_data) + ) r = self.client.post(url, post_data) self.assertEqual(r.status_code, 302) draft = Document.objects.get(pk=draft.pk) @@ -1008,25 +1426,26 @@ def test_edit_authors_permissions(self): def make_edit_authors_post_data(self, basis, authors): """Helper to generate edit_authors POST data for a set of authors""" + def _add_prefix(s): # The prefix here needs to match the formset prefix in the edit_authors() view - return 'author-{}'.format(s) + return "author-{}".format(s) data = { - 'basis': basis, + "basis": basis, # management form - _add_prefix('TOTAL_FORMS'): '1', # just the empty form so far - _add_prefix('INITIAL_FORMS'): str(len(authors)), - _add_prefix('MIN_NUM_FORMS'): '0', - _add_prefix('MAX_NUM_FORMS'): '1000', + _add_prefix("TOTAL_FORMS"): "1", # just the empty form so far + _add_prefix("INITIAL_FORMS"): str(len(authors)), + _add_prefix("MIN_NUM_FORMS"): "0", + _add_prefix("MAX_NUM_FORMS"): "1000", # empty form - _add_prefix('__prefix__-person'): '', - _add_prefix('__prefix__-email'): '', - _add_prefix('__prefix__-affiliation'): '', - _add_prefix('__prefix__-country'): '', - _add_prefix('__prefix__-ORDER'): '', + _add_prefix("__prefix__-person"): "", + _add_prefix("__prefix__-email"): "", + _add_prefix("__prefix__-affiliation"): "", + _add_prefix("__prefix__-country"): "", + _add_prefix("__prefix__-ORDER"): "", } - + for index, auth in enumerate(authors): self.add_author_to_edit_authors_post_data( data, @@ -1034,96 +1453,114 @@ def _add_prefix(s): person=str(auth.person.pk), email=auth.email, affiliation=auth.affiliation, - country=auth.country - ) + country=auth.country, + ), ) - + return data - - def add_author_to_edit_authors_post_data(self, post_data, new_author, insert_order=-1, prefix='author'): + + def add_author_to_edit_authors_post_data( + self, post_data, new_author, insert_order=-1, prefix="author" + ): """Helper to insert an author in the POST data for the edit_authors view - + The insert_order parameter is 0-indexed (i.e., it's the Django formset ORDER field, not the DocumentAuthor order property, which is 1-indexed) """ + def _add_prefix(s): - return '{}-{}'.format(prefix, s) + return "{}-{}".format(prefix, s) - total_forms = int(post_data[_add_prefix('TOTAL_FORMS')]) - 1 # subtract 1 for empty form + total_forms = ( + int(post_data[_add_prefix("TOTAL_FORMS")]) - 1 + ) # subtract 1 for empty form if insert_order < 0: insert_order = total_forms else: # Make a map from order to the data key that has that order value order_key = dict() for order in range(insert_order, total_forms): - key = _add_prefix(str(order) + '-ORDER') + key = _add_prefix(str(order) + "-ORDER") order_key[int(post_data[key])] = key # now increment all orders at or above where new element will be inserted for order in range(insert_order, total_forms): post_data[order_key[order]] = str(order + 1) - + form_index = total_forms # regardless of insert order, new data has next unused form index total_forms += 1 # new form - post_data[_add_prefix('TOTAL_FORMS')] = total_forms + 1 # add 1 for empty form - for prop in ['person', 'email', 'affiliation', 'country']: - post_data[_add_prefix(str(form_index) + '-' + prop)] = str(new_author[prop]) - post_data[_add_prefix(str(form_index) + '-ORDER')] = str(insert_order) + post_data[_add_prefix("TOTAL_FORMS")] = total_forms + 1 # add 1 for empty form + for prop in ["person", "email", "affiliation", "country"]: + post_data[_add_prefix(str(form_index) + "-" + prop)] = str(new_author[prop]) + post_data[_add_prefix(str(form_index) + "-ORDER")] = str(insert_order) def test_edit_authors_missing_basis(self): draft = WgDraftFactory() DocumentAuthorFactory.create_batch(3, document=draft) - url = urlreverse('ietf.doc.views_doc.edit_authors', kwargs=dict(name=draft.name)) + url = urlreverse( + "ietf.doc.views_doc.edit_authors", kwargs=dict(name=draft.name) + ) - self.login('secretary') + self.login("secretary") post_data = self.make_edit_authors_post_data( - authors = draft.documentauthor_set.all(), - basis='delete me' + authors=draft.documentauthor_set.all(), basis="delete me" ) - post_data.pop('basis') + post_data.pop("basis") r = self.client.post(url, post_data) self.assertEqual(r.status_code, 200) - self.assertContains(r, 'This field is required.') + self.assertContains(r, "This field is required.") def test_edit_authors_no_change(self): draft = WgDraftFactory() DocumentAuthorFactory.create_batch(3, document=draft) - url = urlreverse('ietf.doc.views_doc.edit_authors', kwargs=dict(name=draft.name)) - change_reason = 'no change' + url = urlreverse( + "ietf.doc.views_doc.edit_authors", kwargs=dict(name=draft.name) + ) + change_reason = "no change" - before = list(draft.documentauthor_set.values('person', 'email', 'affiliation', 'country', 'order')) + before = list( + draft.documentauthor_set.values( + "person", "email", "affiliation", "country", "order" + ) + ) post_data = self.make_edit_authors_post_data( - authors = draft.documentauthor_set.all(), - basis=change_reason + authors=draft.documentauthor_set.all(), basis=change_reason ) - self.login('secretary') + self.login("secretary") r = self.client.post(url, post_data) self.assertEqual(r.status_code, 302) draft = Document.objects.get(pk=draft.pk) - after = list(draft.documentauthor_set.values('person', 'email', 'affiliation', 'country', 'order')) - self.assertCountEqual(after, before, 'Unexpected change to an author') - self.assertEqual(EditedAuthorsDocEvent.objects.filter(basis=change_reason).count(), 0) + after = list( + draft.documentauthor_set.values( + "person", "email", "affiliation", "country", "order" + ) + ) + self.assertCountEqual(after, before, "Unexpected change to an author") + self.assertEqual( + EditedAuthorsDocEvent.objects.filter(basis=change_reason).count(), 0 + ) def do_edit_authors_append_authors_test(self, new_author_count): """Can add author at the end of the list""" draft = WgDraftFactory() starting_author_count = 3 DocumentAuthorFactory.create_batch(starting_author_count, document=draft) - url = urlreverse('ietf.doc.views_doc.edit_authors', kwargs=dict(name=draft.name)) - change_reason = 'add a new author' + url = urlreverse( + "ietf.doc.views_doc.edit_authors", kwargs=dict(name=draft.name) + ) + change_reason = "add a new author" - compare_props = 'person', 'email', 'affiliation', 'country', 'order' + compare_props = "person", "email", "affiliation", "country", "order" before = list(draft.documentauthor_set.values(*compare_props)) events_before = EditedAuthorsDocEvent.objects.count() post_data = self.make_edit_authors_post_data( - authors=draft.documentauthor_set.all(), - basis=change_reason + authors=draft.documentauthor_set.all(), basis=change_reason ) new_authors = PersonFactory.create_batch(new_author_count, default_emails=True) @@ -1131,16 +1568,18 @@ def do_edit_authors_append_authors_test(self, new_author_count): dict( person=new_author.pk, email=str(new_author.email()), - affiliation='University of Somewhere', - country='Botswana', + affiliation="University of Somewhere", + country="Botswana", ) for new_author in new_authors ] for index, auth_dict in enumerate(new_author_data): self.add_author_to_edit_authors_post_data(post_data, auth_dict) - auth_dict['order'] = starting_author_count + index + 1 # for comparison later + auth_dict["order"] = ( + starting_author_count + index + 1 + ) # for comparison later - self.login('secretary') + self.login("secretary") r = self.client.post(url, post_data) self.assertEqual(r.status_code, 302) @@ -1150,18 +1589,23 @@ def do_edit_authors_append_authors_test(self, new_author_count): self.assertEqual(len(after), len(before) + new_author_count) for b, a in zip(before + new_author_data, after): for prop in compare_props: - self.assertEqual(a[prop], b[prop], - 'Unexpected change: "{}" was "{}", changed to "{}"'.format( - prop, b[prop], a[prop] - )) + self.assertEqual( + a[prop], + b[prop], + 'Unexpected change: "{}" was "{}", changed to "{}"'.format( + prop, b[prop], a[prop] + ), + ) - self.assertEqual(EditedAuthorsDocEvent.objects.count(), events_before + new_author_count) + self.assertEqual( + EditedAuthorsDocEvent.objects.count(), events_before + new_author_count + ) change_events = EditedAuthorsDocEvent.objects.filter(basis=change_reason) self.assertEqual(change_events.count(), new_author_count) # The events are most-recent first, so first author added is last event in the list. # Reverse the author list with [::-1] for evt, auth in zip(change_events, new_authors[::-1]): - self.assertIn('added', evt.desc.lower()) + self.assertIn("added", evt.desc.lower()) self.assertIn(auth.name, evt.desc) def test_edit_authors_append_author(self): @@ -1174,128 +1618,139 @@ def test_edit_authors_insert_author(self): """Can add author in the middle of the list""" draft = WgDraftFactory() DocumentAuthorFactory.create_batch(3, document=draft) - url = urlreverse('ietf.doc.views_doc.edit_authors', kwargs=dict(name=draft.name)) - change_reason = 'add a new author' + url = urlreverse( + "ietf.doc.views_doc.edit_authors", kwargs=dict(name=draft.name) + ) + change_reason = "add a new author" - compare_props = 'person', 'email', 'affiliation', 'country', 'order' + compare_props = "person", "email", "affiliation", "country", "order" before = list(draft.documentauthor_set.values(*compare_props)) events_before = EditedAuthorsDocEvent.objects.count() post_data = self.make_edit_authors_post_data( - authors = draft.documentauthor_set.all(), - basis=change_reason + authors=draft.documentauthor_set.all(), basis=change_reason ) new_author = PersonFactory(default_emails=True) new_author_data = dict( person=new_author.pk, email=str(new_author.email()), - affiliation='University of Somewhere', - country='Botswana', + affiliation="University of Somewhere", + country="Botswana", + ) + self.add_author_to_edit_authors_post_data( + post_data, new_author_data, insert_order=1 ) - self.add_author_to_edit_authors_post_data(post_data, new_author_data, insert_order=1) - self.login('secretary') + self.login("secretary") r = self.client.post(url, post_data) self.assertEqual(r.status_code, 302) draft = Document.objects.get(pk=draft.pk) after = list(draft.documentauthor_set.values(*compare_props)) - new_author_data['order'] = 2 # corresponds to insert_order == 1 + new_author_data["order"] = 2 # corresponds to insert_order == 1 expected = copy.deepcopy(before) expected.insert(1, new_author_data) - expected[2]['order'] = 3 - expected[3]['order'] = 4 + expected[2]["order"] = 3 + expected[3]["order"] = 4 self.assertEqual(len(after), len(expected)) for b, a in zip(expected, after): for prop in compare_props: - self.assertEqual(a[prop], b[prop], - 'Unexpected change: "{}" was "{}", changed to "{}"'.format( - prop, b[prop], a[prop] - )) + self.assertEqual( + a[prop], + b[prop], + 'Unexpected change: "{}" was "{}", changed to "{}"'.format( + prop, b[prop], a[prop] + ), + ) # 3 changes: new author, plus two order changes self.assertEqual(EditedAuthorsDocEvent.objects.count(), events_before + 3) change_events = EditedAuthorsDocEvent.objects.filter(basis=change_reason) self.assertEqual(change_events.count(), 3) - - add_event = change_events.filter(desc__icontains='added').first() - reorder_events = change_events.filter(desc__icontains='changed order') - + + add_event = change_events.filter(desc__icontains="added").first() + reorder_events = change_events.filter(desc__icontains="changed order") + self.assertIsNotNone(add_event) self.assertEqual(reorder_events.count(), 2) def test_edit_authors_remove_author(self): draft = WgDraftFactory() DocumentAuthorFactory.create_batch(3, document=draft) - url = urlreverse('ietf.doc.views_doc.edit_authors', kwargs=dict(name=draft.name)) - change_reason = 'remove an author' + url = urlreverse( + "ietf.doc.views_doc.edit_authors", kwargs=dict(name=draft.name) + ) + change_reason = "remove an author" - compare_props = 'person', 'email', 'affiliation', 'country', 'order' + compare_props = "person", "email", "affiliation", "country", "order" before = list(draft.documentauthor_set.values(*compare_props)) events_before = EditedAuthorsDocEvent.objects.count() post_data = self.make_edit_authors_post_data( - authors = draft.documentauthor_set.all(), - basis=change_reason + authors=draft.documentauthor_set.all(), basis=change_reason ) # delete the second author (index == 1) deleted_author_data = before.pop(1) - post_data['author-1-DELETE'] = 'on' # delete box checked + post_data["author-1-DELETE"] = "on" # delete box checked - self.login('secretary') + self.login("secretary") r = self.client.post(url, post_data) self.assertEqual(r.status_code, 302) draft = Document.objects.get(pk=draft.pk) after = list(draft.documentauthor_set.values(*compare_props)) - before[1]['order'] = 2 # was 3, but should have been decremented + before[1]["order"] = 2 # was 3, but should have been decremented self.assertEqual(len(after), len(before)) for b, a in zip(before, after): for prop in compare_props: - self.assertEqual(a[prop], b[prop], - 'Unexpected change: "{}" was "{}", changed to "{}"'.format( - prop, b[prop], a[prop] - )) + self.assertEqual( + a[prop], + b[prop], + 'Unexpected change: "{}" was "{}", changed to "{}"'.format( + prop, b[prop], a[prop] + ), + ) # expect 2 events: one for removing author, another for reordering the later author self.assertEqual(EditedAuthorsDocEvent.objects.count(), events_before + 2) change_events = EditedAuthorsDocEvent.objects.filter(basis=change_reason) self.assertEqual(change_events.count(), 2) - removed_event = change_events.filter(desc__icontains='removed').first() + removed_event = change_events.filter(desc__icontains="removed").first() self.assertIsNotNone(removed_event) - deleted_person = Person.objects.get(pk=deleted_author_data['person']) + deleted_person = Person.objects.get(pk=deleted_author_data["person"]) self.assertIn(deleted_person.name, removed_event.desc) - reordered_event = change_events.filter(desc__icontains='changed order').first() - reordered_person = Person.objects.get(pk=after[1]['person']) + reordered_event = change_events.filter(desc__icontains="changed order").first() + reordered_person = Person.objects.get(pk=after[1]["person"]) self.assertIsNotNone(reordered_event) self.assertIn(reordered_person.name, reordered_event.desc) def test_edit_authors_reorder_authors(self): draft = WgDraftFactory() DocumentAuthorFactory.create_batch(3, document=draft) - url = urlreverse('ietf.doc.views_doc.edit_authors', kwargs=dict(name=draft.name)) - change_reason = 'reorder the authors' + url = urlreverse( + "ietf.doc.views_doc.edit_authors", kwargs=dict(name=draft.name) + ) + change_reason = "reorder the authors" - compare_props = 'person', 'email', 'affiliation', 'country', 'order' + compare_props = "person", "email", "affiliation", "country", "order" before = list(draft.documentauthor_set.values(*compare_props)) events_before = EditedAuthorsDocEvent.objects.count() post_data = self.make_edit_authors_post_data( - authors = draft.documentauthor_set.all(), - basis=change_reason + authors=draft.documentauthor_set.all(), basis=change_reason ) - + # swap first two authors - post_data['author-0-ORDER'] = 1 - post_data['author-1-ORDER'] = 0 + post_data["author-0-ORDER"] = 1 + post_data["author-1-ORDER"] = 0 - self.login('secretary') + self.login("secretary") r = self.client.post(url, post_data) self.assertEqual(r.status_code, 302) @@ -1305,30 +1760,35 @@ def test_edit_authors_reorder_authors(self): # swap the 'before' record order tmp = before[0] before[0] = before[1] - before[0]['order'] = 1 + before[0]["order"] = 1 before[1] = tmp - before[1]['order'] = 2 + before[1]["order"] = 2 for b, a in zip(before, after): for prop in compare_props: - self.assertEqual(a[prop], b[prop], - 'Unexpected change: "{}" was "{}", changed to "{}"'.format( - prop, b[prop], a[prop] - )) + self.assertEqual( + a[prop], + b[prop], + 'Unexpected change: "{}" was "{}", changed to "{}"'.format( + prop, b[prop], a[prop] + ), + ) # expect 2 events: one for each changed author self.assertEqual(EditedAuthorsDocEvent.objects.count(), events_before + 2) change_events = EditedAuthorsDocEvent.objects.filter(basis=change_reason) self.assertEqual(change_events.count(), 2) - self.assertEqual(change_events.filter(desc__icontains='changed order').count(), 2) + self.assertEqual( + change_events.filter(desc__icontains="changed order").count(), 2 + ) self.assertIsNotNone( change_events.filter( - desc__contains=Person.objects.get(pk=before[0]['person']).name + desc__contains=Person.objects.get(pk=before[0]["person"]).name ).first() ) self.assertIsNotNone( change_events.filter( - desc__contains=Person.objects.get(pk=before[1]['person']).name + desc__contains=Person.objects.get(pk=before[1]["person"]).name ).first() ) @@ -1337,28 +1797,31 @@ def test_edit_authors_edit_fields(self): DocumentAuthorFactory.create_batch( 3, document=draft, - affiliation='Somewhere, Inc.', - country='Bolivia', + affiliation="Somewhere, Inc.", + country="Bolivia", + ) + url = urlreverse( + "ietf.doc.views_doc.edit_authors", kwargs=dict(name=draft.name) ) - url = urlreverse('ietf.doc.views_doc.edit_authors', kwargs=dict(name=draft.name)) - change_reason = 'reorder the authors' + change_reason = "reorder the authors" - compare_props = 'person', 'email', 'affiliation', 'country', 'order' + compare_props = "person", "email", "affiliation", "country", "order" before = list(draft.documentauthor_set.values(*compare_props)) events_before = EditedAuthorsDocEvent.objects.count() post_data = self.make_edit_authors_post_data( - authors = draft.documentauthor_set.all(), - basis=change_reason + authors=draft.documentauthor_set.all(), basis=change_reason ) old_address = draft.authors()[0].email() - new_email = EmailFactory(person=draft.authors()[0], address=f'changed-{old_address}') - post_data['author-0-email'] = new_email.address - post_data['author-1-affiliation'] = 'University of Nowhere' - post_data['author-2-country'] = 'Chile' + new_email = EmailFactory( + person=draft.authors()[0], address=f"changed-{old_address}" + ) + post_data["author-0-email"] = new_email.address + post_data["author-1-affiliation"] = "University of Nowhere" + post_data["author-2-country"] = "Chile" - self.login('secretary') + self.login("secretary") r = self.client.post(url, post_data) self.assertEqual(r.status_code, 302) @@ -1366,73 +1829,95 @@ def test_edit_authors_edit_fields(self): after = list(draft.documentauthor_set.values(*compare_props)) expected = copy.deepcopy(before) - expected[0]['email'] = new_email.address - expected[1]['affiliation'] = 'University of Nowhere' - expected[2]['country'] = 'Chile' + expected[0]["email"] = new_email.address + expected[1]["affiliation"] = "University of Nowhere" + expected[2]["country"] = "Chile" for b, a in zip(expected, after): for prop in compare_props: - self.assertEqual(a[prop], b[prop], - 'Unexpected change: "{}" was "{}", changed to "{}"'.format( - prop, b[prop], a[prop] - )) + self.assertEqual( + a[prop], + b[prop], + 'Unexpected change: "{}" was "{}", changed to "{}"'.format( + prop, b[prop], a[prop] + ), + ) # expect 3 events: one for each changed author self.assertEqual(EditedAuthorsDocEvent.objects.count(), events_before + 3) change_events = EditedAuthorsDocEvent.objects.filter(basis=change_reason) self.assertEqual(change_events.count(), 3) - email_event = change_events.filter(desc__icontains='changed email').first() - affiliation_event = change_events.filter(desc__icontains='changed affiliation').first() - country_event = change_events.filter(desc__icontains='changed country').first() + email_event = change_events.filter(desc__icontains="changed email").first() + affiliation_event = change_events.filter( + desc__icontains="changed affiliation" + ).first() + country_event = change_events.filter(desc__icontains="changed country").first() self.assertIsNotNone(email_event) self.assertIn(draft.authors()[0].name, email_event.desc) - self.assertIn(before[0]['email'], email_event.desc) - self.assertIn(after[0]['email'], email_event.desc) + self.assertIn(before[0]["email"], email_event.desc) + self.assertIn(after[0]["email"], email_event.desc) self.assertIsNotNone(affiliation_event) self.assertIn(draft.authors()[1].name, affiliation_event.desc) - self.assertIn(before[1]['affiliation'], affiliation_event.desc) - self.assertIn(after[1]['affiliation'], affiliation_event.desc) + self.assertIn(before[1]["affiliation"], affiliation_event.desc) + self.assertIn(after[1]["affiliation"], affiliation_event.desc) self.assertIsNotNone(country_event) self.assertIn(draft.authors()[2].name, country_event.desc) - self.assertIn(before[2]['country'], country_event.desc) - self.assertIn(after[2]['country'], country_event.desc) + self.assertIn(before[2]["country"], country_event.desc) + self.assertIn(after[2]["country"], country_event.desc) @staticmethod def _pyquery_select_action_holder_string(q, s): """Helper to use PyQuery to find an action holder in the draft HTML""" # selector grabs the action holders heading and finds siblings with a div containing the search string (also in any title attribute) - return q('th:contains("Action Holder") ~ td>div:contains("%s"), th:contains("Action Holder") ~ td>div *[title*="%s"]' % (s, s)) + return q( + 'th:contains("Action Holder") ~ td>div:contains("%s"), th:contains("Action Holder") ~ td>div *[title*="%s"]' + % (s, s) + ) - @mock.patch.object(Document, 'action_holders_enabled', return_value=False, new_callable=mock.PropertyMock) + @mock.patch.object( + Document, + "action_holders_enabled", + return_value=False, + new_callable=mock.PropertyMock, + ) def test_document_draft_hides_action_holders(self, mock_method): """Draft should not show action holders when appropriate""" draft = WgDraftFactory() - url = urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + url = urlreverse( + "ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name) + ) r = self.client.get(url) - self.assertNotContains(r, 'Action Holder') # should not show action holders... + self.assertNotContains(r, "Action Holder") # should not show action holders... draft.action_holders.set([PersonFactory()]) r = self.client.get(url) - self.assertNotContains(r, 'Action Holder') # ...even if they are assigned - - @mock.patch.object(Document, 'action_holders_enabled', return_value=True, new_callable=mock.PropertyMock) + self.assertNotContains(r, "Action Holder") # ...even if they are assigned + + @mock.patch.object( + Document, + "action_holders_enabled", + return_value=True, + new_callable=mock.PropertyMock, + ) def test_document_draft_shows_action_holders(self, mock_method): """Draft should show action holders when appropriate""" draft = WgDraftFactory() - url = urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + url = urlreverse( + "ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name) + ) # No action holders case should be shown properly r = self.client.get(url) - self.assertContains(r, 'Action Holder') # should show action holders + self.assertContains(r, "Action Holder") # should show action holders q = PyQuery(r.content) - self.assertEqual(len(self._pyquery_select_action_holder_string(q, '(None)')), 1) - + self.assertEqual(len(self._pyquery_select_action_holder_string(q, "(None)")), 1) + # Action holders should be listed when assigned draft.action_holders.set(PersonFactory.create_batch(3)) - + # Make one action holder "old" old_action_holder = draft.documentactionholder_set.first() old_action_holder.time_added -= datetime.timedelta(days=30) @@ -1441,15 +1926,24 @@ def test_document_draft_shows_action_holders(self, mock_method): with self.settings(DOC_ACTION_HOLDER_AGE_LIMIT_DAYS=20): r = self.client.get(url) - self.assertContains(r, 'Action Holder') # should still be shown + self.assertContains(r, "Action Holder") # should still be shown q = PyQuery(r.content) - self.assertEqual(len(self._pyquery_select_action_holder_string(q, '(None)')), 0) + self.assertEqual(len(self._pyquery_select_action_holder_string(q, "(None)")), 0) for person in draft.action_holders.all(): - self.assertEqual(len(self._pyquery_select_action_holder_string(q, person.name)), 1) + self.assertEqual( + len(self._pyquery_select_action_holder_string(q, person.name)), 1 + ) # check that one action holder was marked as old - self.assertEqual(len(self._pyquery_select_action_holder_string(q, 'for 30 days')), 1) + self.assertEqual( + len(self._pyquery_select_action_holder_string(q, "for 30 days")), 1 + ) - @mock.patch.object(Document, 'action_holders_enabled', return_value=True, new_callable=mock.PropertyMock) + @mock.patch.object( + Document, + "action_holders_enabled", + return_value=True, + new_callable=mock.PropertyMock, + ) def test_document_draft_action_holders_buttons(self, mock_method): """Buttons for action holders should be shown when AD or secretary""" draft = WgDraftFactory() @@ -1461,87 +1955,125 @@ def test_document_draft_action_holders_buttons(self, mock_method): draft.group.features.docman_roles.append("wrangler") draft.group.features.save() wrangler = RoleFactory(group=draft.group, name_id="wrangler").person - wrangler_of_other_group = RoleFactory(group=other_group, name_id="wrangler").person + wrangler_of_other_group = RoleFactory( + group=other_group, name_id="wrangler" + ).person - url = urlreverse('ietf.doc.views_doc.document_main', kwargs=dict(name=draft.name)) - edit_ah_url = urlreverse('ietf.doc.views_doc.edit_action_holders', kwargs=dict(name=draft.name)) - remind_ah_url = urlreverse('ietf.doc.views_doc.remind_action_holders', kwargs=dict(name=draft.name)) + url = urlreverse( + "ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name) + ) + edit_ah_url = urlreverse( + "ietf.doc.views_doc.edit_action_holders", kwargs=dict(name=draft.name) + ) + remind_ah_url = urlreverse( + "ietf.doc.views_doc.remind_action_holders", kwargs=dict(name=draft.name) + ) def _run_test(username=None, expect_buttons=False): if username: - self.client.login(username=username, password=username + '+password') + self.client.login(username=username, password=username + "+password") r = self.client.get(url) q = PyQuery(r.content) self.assertEqual( len(q('th:contains("Action Holder") ~ td a[href="%s"]' % edit_ah_url)), 1 if expect_buttons else 0, - '%s should%s see the edit action holders button but %s' % ( - username if username else 'unauthenticated user', - '' if expect_buttons else ' not', - 'did not' if expect_buttons else 'did', - ) + "%s should%s see the edit action holders button but %s" + % ( + username if username else "unauthenticated user", + "" if expect_buttons else " not", + "did not" if expect_buttons else "did", + ), ) self.assertEqual( - len(q('th:contains("Action Holder") ~ td a[href="%s"]' % remind_ah_url)), + len( + q('th:contains("Action Holder") ~ td a[href="%s"]' % remind_ah_url) + ), 1 if expect_buttons else 0, - '%s should%s see the remind action holders button but %s' % ( - username if username else 'unauthenticated user', - '' if expect_buttons else ' not', - 'did not' if expect_buttons else 'did', - ) + "%s should%s see the remind action holders button but %s" + % ( + username if username else "unauthenticated user", + "" if expect_buttons else " not", + "did not" if expect_buttons else "did", + ), ) _run_test(None, False) - _run_test('plain', False) + _run_test("plain", False) _run_test(wrangler_of_other_group.user.username, False) _run_test(wrangler.user.username, True) - _run_test('ad', True) - _run_test('secretary', True) + _run_test("ad", True) + _run_test("secretary", True) def test_draft_group_link(self): """Link to group 'about' page should have correct format""" event_datetime = datetime.datetime(2010, 10, 10, tzinfo=RPC_TZINFO) - for group_type_id in ['wg', 'rg', 'ag']: + for group_type_id in ["wg", "rg", "ag"]: group = GroupFactory(type_id=group_type_id) - draft = WgDraftFactory(name='draft-document-%s' % group_type_id, group=group) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) + draft = WgDraftFactory( + name="draft-document-%s" % group_type_id, group=group + ) + r = self.client.get( + urlreverse( + "ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name) + ) + ) self.assertEqual(r.status_code, 200) self.assert_correct_wg_group_link(r, group) rfc = WgRfcFactory(group=group) draft = WgDraftFactory(group=group) draft.relateddocument_set.create(relationship_id="became_rfc", target=rfc) - DocEventFactory.create(doc=rfc, type='published_rfc', time=event_datetime) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=rfc.name))) + DocEventFactory.create(doc=rfc, type="published_rfc", time=event_datetime) + r = self.client.get( + urlreverse( + "ietf.doc.views_doc.document_main", kwargs=dict(name=rfc.name) + ) + ) self.assertEqual(r.status_code, 200) self.assert_correct_wg_group_link(r, group) - for group_type_id in ['ietf', 'team']: + for group_type_id in ["ietf", "team"]: group = GroupFactory(type_id=group_type_id) - draft = WgDraftFactory(name='draft-document-%s' % group_type_id, group=group) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) + draft = WgDraftFactory( + name="draft-document-%s" % group_type_id, group=group + ) + r = self.client.get( + urlreverse( + "ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name) + ) + ) self.assertEqual(r.status_code, 200) self.assert_correct_non_wg_group_link(r, group) rfc = WgRfcFactory(group=group) - draft = WgDraftFactory(name='draft-rfc-document-%s'% group_type_id, group=group) + draft = WgDraftFactory( + name="draft-rfc-document-%s" % group_type_id, group=group + ) draft.relateddocument_set.create(relationship_id="became_rfc", target=rfc) - DocEventFactory.create(doc=rfc, type='published_rfc', time=event_datetime) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=rfc.name))) + DocEventFactory.create(doc=rfc, type="published_rfc", time=event_datetime) + r = self.client.get( + urlreverse( + "ietf.doc.views_doc.document_main", kwargs=dict(name=rfc.name) + ) + ) self.assertEqual(r.status_code, 200) self.assert_correct_non_wg_group_link(r, group) def test_document_email_authors_button(self): # rfc not from draft rfc = WgRfcFactory() - DocEventFactory.create(doc=rfc, type='published_rfc') + DocEventFactory.create(doc=rfc, type="published_rfc") url = urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=rfc.name)) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertEqual(len(q('a:contains("Email authors")')), 0, 'Did not expect "Email authors" button') + self.assertEqual( + len(q('a:contains("Email authors")')), + 0, + 'Did not expect "Email authors" button', + ) # rfc from draft draft = WgDraftFactory(group=rfc.group) @@ -1550,64 +2082,111 @@ def test_document_email_authors_button(self): r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertEqual(len(q('a:contains("Email authors")')), 1, 'Expected "Email authors" button') + self.assertEqual( + len(q('a:contains("Email authors")')), 1, 'Expected "Email authors" button' + ) def test_document_primary_and_history_views(self): - IndividualDraftFactory(name='draft-imaginary-independent-submission') - ConflictReviewFactory(name='conflict-review-imaginary-irtf-submission') - CharterFactory(name='charter-ietf-mars') - DocumentFactory(type_id='agenda',name='agenda-72-mars') - DocumentFactory(type_id='minutes',name='minutes-72-mars') - DocumentFactory(type_id='slides',name='slides-72-mars-1-active') - chatlog = DocumentFactory(type_id="chatlog",name='chatlog-72-mars-197001010000') - polls = DocumentFactory(type_id="polls",name='polls-72-mars-197001010000') + IndividualDraftFactory(name="draft-imaginary-independent-submission") + ConflictReviewFactory(name="conflict-review-imaginary-irtf-submission") + CharterFactory(name="charter-ietf-mars") + DocumentFactory(type_id="agenda", name="agenda-72-mars") + DocumentFactory(type_id="minutes", name="minutes-72-mars") + DocumentFactory(type_id="slides", name="slides-72-mars-1-active") + chatlog = DocumentFactory( + type_id="chatlog", name="chatlog-72-mars-197001010000" + ) + polls = DocumentFactory(type_id="polls", name="polls-72-mars-197001010000") SessionPresentationFactory(document=chatlog) SessionPresentationFactory(document=polls) - statchg = DocumentFactory(type_id='statchg',name='status-change-imaginary-mid-review') - statchg.set_state(State.objects.get(type_id='statchg',slug='adrev')) + statchg = DocumentFactory( + type_id="statchg", name="status-change-imaginary-mid-review" + ) + statchg.set_state(State.objects.get(type_id="statchg", slug="adrev")) # Ensure primary views of both current and historic versions of documents works - for docname in ["draft-imaginary-independent-submission", - "conflict-review-imaginary-irtf-submission", - "status-change-imaginary-mid-review", - "charter-ietf-mars", - "agenda-72-mars", - "minutes-72-mars", - "slides-72-mars-1-active", - "chatlog-72-mars-197001010000", - "polls-72-mars-197001010000", - # TODO: add - #"bluesheets-72-mars-1", - #"recording-72-mars-1-00", - ]: + for docname in [ + "draft-imaginary-independent-submission", + "conflict-review-imaginary-irtf-submission", + "status-change-imaginary-mid-review", + "charter-ietf-mars", + "agenda-72-mars", + "minutes-72-mars", + "slides-72-mars-1-active", + "chatlog-72-mars-197001010000", + "polls-72-mars-197001010000", + # TODO: add + # "bluesheets-72-mars-1", + # "recording-72-mars-1-00", + ]: doc = Document.objects.get(name=docname) # give it some history - doc.save_with_history([DocEvent.objects.create(doc=doc, rev=doc.rev, type="changed_document", by=Person.objects.get(user__username="secretary"), desc="Test")]) + doc.save_with_history( + [ + DocEvent.objects.create( + doc=doc, + rev=doc.rev, + type="changed_document", + by=Person.objects.get(user__username="secretary"), + desc="Test", + ) + ] + ) doc.rev = "01" - doc.save_with_history([DocEvent.objects.create(doc=doc, rev=doc.rev, type="changed_document", by=Person.objects.get(user__username="secretary"), desc="Test")]) + doc.save_with_history( + [ + DocEvent.objects.create( + doc=doc, + rev=doc.rev, + type="changed_document", + by=Person.objects.get(user__username="secretary"), + desc="Test", + ) + ] + ) # Fetch the main page resulting latest version - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name))) + r = self.client.get( + urlreverse( + "ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name) + ) + ) self.assertEqual(r.status_code, 200) - self.assertContains(r, "%s-01"%docname) + self.assertContains(r, "%s-01" % docname) # Fetch 01 version even when it is last version - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name,rev="01"))) + r = self.client.get( + urlreverse( + "ietf.doc.views_doc.document_main", + kwargs=dict(name=doc.name, rev="01"), + ) + ) self.assertEqual(r.status_code, 200) - self.assertContains(r, "%s-01"%docname) + self.assertContains(r, "%s-01" % docname) # Fetch version number which is too large, that should redirect to main page - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name,rev="02"))) + r = self.client.get( + urlreverse( + "ietf.doc.views_doc.document_main", + kwargs=dict(name=doc.name, rev="02"), + ) + ) self.assertEqual(r.status_code, 302) # Fetch 00 version which should result that version - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name,rev="00"))) + r = self.client.get( + urlreverse( + "ietf.doc.views_doc.document_main", + kwargs=dict(name=doc.name, rev="00"), + ) + ) self.assertEqual(r.status_code, 200) - self.assertContains(r, "%s-00"%docname) + self.assertContains(r, "%s-00" % docname) def test_rfcqueue_auth48_views(self): """Test view handling of RFC editor queue auth48 state""" + def _change_state(doc, state): event = StateDocEventFactory(doc=doc, state=state) doc.set_state(event.state) @@ -1616,108 +2195,156 @@ def _change_state(doc, state): draft = IndividualDraftFactory() # Put in an rfceditor state other than auth48 - for state in [('draft-iesg', 'rfcqueue'), ('draft-rfceditor', 'rfc-edit')]: + for state in [("draft-iesg", "rfcqueue"), ("draft-rfceditor", "rfc-edit")]: _change_state(draft, state) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + ) self.assertEqual(r.status_code, 200) - self.assertNotContains(r, 'Auth48 status') + self.assertNotContains(r, "Auth48 status") # Put in auth48 state without a URL - _change_state(draft, ('draft-rfceditor', 'auth48')) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) + _change_state(draft, ("draft-rfceditor", "auth48")) + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + ) self.assertEqual(r.status_code, 200) - self.assertNotContains(r, 'Auth48 status') + self.assertNotContains(r, "Auth48 status") # Now add a URL - documenturl = draft.documenturl_set.create(tag_id='auth48', - url='http://rfceditor.example.com/auth48-url') - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) + documenturl = draft.documenturl_set.create( + tag_id="auth48", url="http://rfceditor.example.com/auth48-url" + ) + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + ) self.assertEqual(r.status_code, 200) - self.assertContains(r, 'Auth48 status') + self.assertContains(r, "Auth48 status") self.assertContains(r, documenturl.url) # Put in auth48-done state and delete auth48 DocumentURL - draft.documenturl_set.filter(tag_id='auth48').delete() - _change_state(draft, ('draft-rfceditor', 'auth48-done')) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) + draft.documenturl_set.filter(tag_id="auth48").delete() + _change_state(draft, ("draft-rfceditor", "auth48-done")) + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + ) self.assertEqual(r.status_code, 200) - self.assertNotContains(r, 'Auth48 status') + self.assertNotContains(r, "Auth48 status") class DocTestCase(TestCase): def test_status_change(self): statchg = StatusChangeFactory() - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=statchg.name))) + r = self.client.get( + urlreverse( + "ietf.doc.views_doc.document_main", kwargs=dict(name=statchg.name) + ) + ) self.assertEqual(r.status_code, 200) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=statchg.relateddocument_set.first().target))) + r = self.client.get( + urlreverse( + "ietf.doc.views_doc.document_main", + kwargs=dict(name=statchg.relateddocument_set.first().target), + ) + ) self.assertEqual(r.status_code, 200) def test_document_charter(self): - CharterFactory(name='charter-ietf-mars') - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name="charter-ietf-mars"))) + CharterFactory(name="charter-ietf-mars") + r = self.client.get( + urlreverse( + "ietf.doc.views_doc.document_main", + kwargs=dict(name="charter-ietf-mars"), + ) + ) self.assertEqual(r.status_code, 200) - + def test_incorrect_rfc_url(self): - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name="rfc8989", rev="00"))) + r = self.client.get( + urlreverse( + "ietf.doc.views_doc.document_main", + kwargs=dict(name="rfc8989", rev="00"), + ) + ) self.assertEqual(r.status_code, 404) def test_document_conflict_review(self): - ConflictReviewFactory(name='conflict-review-imaginary-irtf-submission') + ConflictReviewFactory(name="conflict-review-imaginary-irtf-submission") - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name='conflict-review-imaginary-irtf-submission'))) + r = self.client.get( + urlreverse( + "ietf.doc.views_doc.document_main", + kwargs=dict(name="conflict-review-imaginary-irtf-submission"), + ) + ) self.assertEqual(r.status_code, 200) def test_document_material(self): - MeetingFactory(type_id='ietf',number='72') - mars = GroupFactory(type_id='wg',acronym='mars') - marschairman = PersonFactory(user__username='marschairman') - mars.role_set.create(name_id='chair',person=marschairman,email=marschairman.email()) + MeetingFactory(type_id="ietf", number="72") + mars = GroupFactory(type_id="wg", acronym="mars") + marschairman = PersonFactory(user__username="marschairman") + mars.role_set.create( + name_id="chair", person=marschairman, email=marschairman.email() + ) doc = DocumentFactory( name="slides-testteam-test-slides", rev="00", title="Test Slides", - group__acronym='testteam', - type_id="slides" + group__acronym="testteam", + type_id="slides", ) doc.set_state(State.objects.get(type="slides", slug="active")) session = SessionFactory( - name = "session-72-mars-1", - meeting = Meeting.objects.get(number='72'), - group = Group.objects.get(acronym='mars'), - modified = timezone.now(), + name="session-72-mars-1", + meeting=Meeting.objects.get(number="72"), + group=Group.objects.get(acronym="mars"), + modified=timezone.now(), add_to_schedule=False, ) SchedulingEvent.objects.create( session=session, - status=SessionStatusName.objects.create(slug='scheduled'), - by = Person.objects.get(user__username="marschairman"), + status=SessionStatusName.objects.create(slug="scheduled"), + by=Person.objects.get(user__username="marschairman"), ) SessionPresentation.objects.create(session=session, document=doc, rev=doc.rev) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name))) + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name)) + ) self.assertEqual(r.status_code, 200) self.assertNotContains(r, "The session for this document was cancelled.") SchedulingEvent.objects.create( session=session, - status_id='canceled', - by = Person.objects.get(user__username="marschairman"), + status_id="canceled", + by=Person.objects.get(user__username="marschairman"), ) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name))) + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name)) + ) self.assertEqual(r.status_code, 200) self.assertContains(r, "The session for this document was cancelled.") def test_document_ballot(self): doc = IndividualDraftFactory() ad = Person.objects.get(user__username="ad") - ballot = create_ballot_if_not_open(None, doc, ad, 'approve') + ballot = create_ballot_if_not_open(None, doc, ad, "approve") assert ballot == doc.active_ballot() # make sure we have some history - doc.save_with_history([DocEvent.objects.create(doc=doc, rev=doc.rev, type="changed_document", - by=Person.objects.get(user__username="secretary"), desc="Test")]) + doc.save_with_history( + [ + DocEvent.objects.create( + doc=doc, + rev=doc.rev, + type="changed_document", + by=Person.objects.get(user__username="secretary"), + desc="Test", + ) + ] + ) pos = BallotPositionDocEvent.objects.create( doc=doc, @@ -1728,34 +2355,57 @@ def test_document_ballot(self): comment="Looks fine to me", comment_time=timezone.now(), balloter=Person.objects.get(user__username="ad"), - by=Person.objects.get(name="(System)")) + by=Person.objects.get(name="(System)"), + ) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name))) + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name)) + ) self.assertEqual(r.status_code, 200) self.assertContains(r, pos.comment) # test with ballot_id - r = self.client.get(urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name, ballot_id=ballot.pk))) + r = self.client.get( + urlreverse( + "ietf.doc.views_doc.document_ballot", + kwargs=dict(name=doc.name, ballot_id=ballot.pk), + ) + ) self.assertEqual(r.status_code, 200) self.assertContains(r, pos.comment) # test popup too while we're at it - r = self.client.get(urlreverse("ietf.doc.views_doc.ballot_popup", kwargs=dict(name=doc.name, ballot_id=ballot.pk))) + r = self.client.get( + urlreverse( + "ietf.doc.views_doc.ballot_popup", + kwargs=dict(name=doc.name, ballot_id=ballot.pk), + ) + ) self.assertEqual(r.status_code, 200) # Now simulate a new revision and make sure positions on older revisions are marked as such oldrev = doc.rev - e = NewRevisionDocEvent.objects.create(doc=doc,rev='%02d'%(int(doc.rev)+1),type='new_revision',by=Person.objects.get(name="(System)")) + e = NewRevisionDocEvent.objects.create( + doc=doc, + rev="%02d" % (int(doc.rev) + 1), + type="new_revision", + by=Person.objects.get(name="(System)"), + ) doc.rev = e.rev doc.save_with_history([e]) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name))) + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name)) + ) self.assertEqual(r.status_code, 200) self.assertRegex( r.content.decode(), - r'\(\s*%s\s+for\s+-%s\s*\)' % ( - pos.comment_time.astimezone(ZoneInfo(settings.TIME_ZONE)).strftime('%Y-%m-%d'), + r"\(\s*%s\s+for\s+-%s\s*\)" + % ( + pos.comment_time.astimezone(ZoneInfo(settings.TIME_ZONE)).strftime( + "%Y-%m-%d" + ), oldrev, - ) + ), ) # Now simulate a new ballot against the new revision and make sure the "was" position is included @@ -1768,18 +2418,21 @@ def test_document_ballot(self): comment="Still looks okay to me", comment_time=timezone.now(), balloter=Person.objects.get(user__username="ad"), - by=Person.objects.get(name="(System)")) + by=Person.objects.get(name="(System)"), + ) - r = self.client.get(urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name))) + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name)) + ) self.assertEqual(r.status_code, 200) self.assertContains(r, pos2.comment) - self.assertContains(r, '(was %s)' % pos.pos) + self.assertContains(r, "(was %s)" % pos.pos) def test_document_ballot_popup_unique_anchors_per_doc(self): """Ballot popup anchors should be different for each document""" ad = Person.objects.get(user__username="ad") docs = IndividualDraftFactory.create_batch(2) - ballots = [create_ballot_if_not_open(None, doc, ad, 'approve') for doc in docs] + ballots = [create_ballot_if_not_open(None, doc, ad, "approve") for doc in docs] for doc, ballot in zip(docs, ballots): BallotPositionDocEvent.objects.create( doc=doc, @@ -1790,75 +2443,105 @@ def test_document_ballot_popup_unique_anchors_per_doc(self): comment="Looks fine to me", comment_time=timezone.now(), balloter=Person.objects.get(user__username="ad"), - by=Person.objects.get(name="(System)")) + by=Person.objects.get(name="(System)"), + ) anchors = set() author_slug = slugify(ad.plain_name()) for doc, ballot in zip(docs, ballots): - r = self.client.get(urlreverse( - "ietf.doc.views_doc.ballot_popup", - kwargs=dict(name=doc.name, ballot_id=ballot.pk) - )) + r = self.client.get( + urlreverse( + "ietf.doc.views_doc.ballot_popup", + kwargs=dict(name=doc.name, ballot_id=ballot.pk), + ) + ) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - href = q(f'div.balloter-name a[href$="{author_slug}"]').attr('href') + href = q(f'div.balloter-name a[href$="{author_slug}"]').attr("href") ids = [ - target.attr('id') + target.attr("id") for target in q(f'div.h5[id$="{author_slug}"]').items() ] - self.assertEqual(len(ids), 1, 'Should be exactly one link for the balloter') - self.assertEqual(href, f'#{ids[0]}', 'Anchor href should match ID') + self.assertEqual(len(ids), 1, "Should be exactly one link for the balloter") + self.assertEqual(href, f"#{ids[0]}", "Anchor href should match ID") anchors.add(href) - self.assertEqual(len(anchors), len(docs), 'Each doc should have a distinct anchor for the balloter') + self.assertEqual( + len(anchors), + len(docs), + "Each doc should have a distinct anchor for the balloter", + ) def test_document_ballot_needed_positions(self): # draft - doc = IndividualDraftFactory(intended_std_level_id='ps') - doc.set_state(State.objects.get(type_id='draft-iesg',slug='iesg-eva')) + doc = IndividualDraftFactory(intended_std_level_id="ps") + doc.set_state(State.objects.get(type_id="draft-iesg", slug="iesg-eva")) ad = Person.objects.get(user__username="ad") - create_ballot_if_not_open(None, doc, ad, 'approve') + create_ballot_if_not_open(None, doc, ad, "approve") - r = self.client.get(urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name))) - self.assertContains(r, 'more YES or NO') - Document.objects.filter(pk=doc.pk).update(intended_std_level='inf') - r = self.client.get(urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name))) - self.assertNotContains(r, 'more YES or NO') + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name)) + ) + self.assertContains(r, "more YES or NO") + Document.objects.filter(pk=doc.pk).update(intended_std_level="inf") + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name)) + ) + self.assertNotContains(r, "more YES or NO") # status change - Document.objects.create(name='rfc9998') - Document.objects.create(name='rfc9999') - doc = DocumentFactory(type_id='statchg',name='status-change-imaginary-mid-review') - iesgeval_pk = str(State.objects.get(slug='iesgeval',type__slug='statchg').pk) + Document.objects.create(name="rfc9998") + Document.objects.create(name="rfc9999") + doc = DocumentFactory( + type_id="statchg", name="status-change-imaginary-mid-review" + ) + iesgeval_pk = str(State.objects.get(slug="iesgeval", type__slug="statchg").pk) empty_outbox() - self.client.login(username='ad', password='ad+password') - r = self.client.post(urlreverse('ietf.doc.views_status_change.change_state',kwargs=dict(name=doc.name)),dict(new_state=iesgeval_pk)) + self.client.login(username="ad", password="ad+password") + r = self.client.post( + urlreverse( + "ietf.doc.views_status_change.change_state", kwargs=dict(name=doc.name) + ), + dict(new_state=iesgeval_pk), + ) self.assertEqual(r.status_code, 302) r = self.client.get(r.headers["location"]) self.assertContains(r, ">IESG Evaluation<") self.assertEqual(len(outbox), 2) - self.assertIn('iesg-secretary',outbox[0]['To']) - self.assertIn('drafts-eval',outbox[1]['To']) + self.assertIn("iesg-secretary", outbox[0]["To"]) + self.assertIn("drafts-eval", outbox[1]["To"]) - doc.relateddocument_set.create(target=Document.objects.get(name='rfc9998'),relationship_id='tohist') - r = self.client.get(urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name))) - self.assertNotContains(r, 'Needs a YES') - self.assertNotContains(r, 'more YES or NO') + doc.relateddocument_set.create( + target=Document.objects.get(name="rfc9998"), relationship_id="tohist" + ) + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name)) + ) + self.assertNotContains(r, "Needs a YES") + self.assertNotContains(r, "more YES or NO") - doc.relateddocument_set.create(target=Document.objects.get(name='rfc9999'),relationship_id='tois') - r = self.client.get(urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name))) - self.assertContains(r, 'more YES or NO') + doc.relateddocument_set.create( + target=Document.objects.get(name="rfc9999"), relationship_id="tois" + ) + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name)) + ) + self.assertContains(r, "more YES or NO") def test_document_json(self): doc = IndividualDraftFactory() - r = self.client.get(urlreverse("ietf.doc.views_doc.document_json", kwargs=dict(name=doc.name))) + r = self.client.get( + urlreverse("ietf.doc.views_doc.document_json", kwargs=dict(name=doc.name)) + ) self.assertEqual(r.status_code, 200) data = r.json() - self.assertEqual(doc.name, data['name']) - self.assertEqual(doc.pages,data['pages']) + self.assertEqual(doc.name, data["name"]) + self.assertEqual(doc.pages, data["pages"]) def test_writeup(self): - doc = IndividualDraftFactory(states = [('draft','active'),('draft-iesg','iesg-eva')],) + doc = IndividualDraftFactory( + states=[("draft", "active"), ("draft-iesg", "iesg-eva")], + ) appr = WriteupDocEvent.objects.create( doc=doc, @@ -1866,7 +2549,8 @@ def test_writeup(self): desc="Changed text", type="changed_ballot_approval_text", text="This is ballot approval text.", - by=Person.objects.get(name="(System)")) + by=Person.objects.get(name="(System)"), + ) notes = WriteupDocEvent.objects.create( doc=doc, @@ -1874,7 +2558,8 @@ def test_writeup(self): desc="Changed text", type="changed_ballot_writeup_text", text="This is ballot writeup notes.", - by=Person.objects.get(name="(System)")) + by=Person.objects.get(name="(System)"), + ) rfced_note = WriteupDocEvent.objects.create( doc=doc, @@ -1882,9 +2567,12 @@ def test_writeup(self): desc="Changed text", type="changed_rfc_editor_note_text", text="This is a note for the RFC Editor.", - by=Person.objects.get(name="(System)")) + by=Person.objects.get(name="(System)"), + ) - url = urlreverse('ietf.doc.views_doc.document_writeup', kwargs=dict(name=doc.name)) + url = urlreverse( + "ietf.doc.views_doc.document_writeup", kwargs=dict(name=doc.name) + ) r = self.client.get(url) self.assertEqual(r.status_code, 200) self.assertContains(r, appr.text) @@ -1911,24 +2599,30 @@ def test_history(self): rev=doc.rev, desc="Something happened.", type="added_comment", - by=Person.objects.get(name="(System)")) + by=Person.objects.get(name="(System)"), + ) - url = urlreverse('ietf.doc.views_doc.document_history', kwargs=dict(name=doc.name)) + url = urlreverse( + "ietf.doc.views_doc.document_history", kwargs=dict(name=doc.name) + ) r = self.client.get(url) self.assertEqual(r.status_code, 200) self.assertContains(r, e.desc) def test_history_bis_00(self): rfc = WgRfcFactory(rfc_number=9090) - bis_draft = WgDraftFactory(name='draft-ietf-{}-{}bis'.format(rfc.group.acronym,rfc.name)) + bis_draft = WgDraftFactory( + name="draft-ietf-{}-{}bis".format(rfc.group.acronym, rfc.name) + ) - url = urlreverse('ietf.doc.views_doc.document_history', kwargs=dict(name=bis_draft.name)) + url = urlreverse( + "ietf.doc.views_doc.document_history", kwargs=dict(name=bis_draft.name) + ) r = self.client.get(url) - self.assertEqual(r.status_code, 200) + self.assertEqual(r.status_code, 200) q = PyQuery(unicontent(r)) - attr1='value="{}"'.format(rfc.name) - self.assertEqual(len(q('option['+attr1+'][selected="selected"]')), 1) - + attr1 = 'value="{}"'.format(rfc.name) + self.assertEqual(len(q("option[" + attr1 + '][selected="selected"]')), 1) def test_document_feed(self): doc = IndividualDraftFactory() @@ -1938,7 +2632,8 @@ def test_document_feed(self): rev=doc.rev, desc="Something happened.", type="added_comment", - by=Person.objects.get(name="(System)")) + by=Person.objects.get(name="(System)"), + ) r = self.client.get("/feed/document-changes/%s/" % doc.name) self.assertEqual(r.status_code, 200) @@ -1952,11 +2647,12 @@ def test_document_feed_with_control_character(self): rev=doc.rev, desc="Something happened involving the \x0b character.", type="added_comment", - by=Person.objects.get(name="(System)")) + by=Person.objects.get(name="(System)"), + ) r = self.client.get("/feed/document-changes/%s/" % doc.name) self.assertEqual(r.status_code, 200) - self.assertContains(r, 'Something happened involving the') + self.assertContains(r, "Something happened involving the") def test_last_call_feed(self): doc = IndividualDraftFactory() @@ -1969,7 +2665,8 @@ def test_last_call_feed(self): desc="Last call\x0b", # include a control character to be sure it does not break anything type="sent_last_call", by=Person.objects.get(user__username="secretary"), - expires=datetime_today(DEADLINE_TZINFO) + datetime.timedelta(days=7)) + expires=datetime_today(DEADLINE_TZINFO) + datetime.timedelta(days=7), + ) r = self.client.get("/feed/last-call/") self.assertEqual(r.status_code, 200) @@ -1980,14 +2677,17 @@ def test_rfc_feed(self): DocEventFactory(doc=rfc, type="published_rfc") r = self.client.get("/feed/rfc/") self.assertTrue(r.status_code, 200) - q = PyQuery(r.content[39:]) # Strip off the xml declaration + q = PyQuery(r.content[39:]) # Strip off the xml declaration self.assertEqual(len(q("item")), 1) item = q("item")[0] media_content = item.findall("{http://search.yahoo.com/mrss/}content") - self.assertEqual(len(media_content),4) + self.assertEqual(len(media_content), 4) types = set([m.attrib["type"] for m in media_content]) - self.assertEqual(types, set(["application/rfc+xml", "text/plain", "text/html", "application/pdf"])) - rfcs_2016 = WgRfcFactory.create_batch(3) # rfc numbers will be well below v3 + self.assertEqual( + types, + set(["application/rfc+xml", "text/plain", "text/html", "application/pdf"]), + ) + rfcs_2016 = WgRfcFactory.create_batch(3) # rfc numbers will be well below v3 for rfc in rfcs_2016: e = DocEventFactory(doc=rfc, type="published_rfc") e.time = e.time.replace(year=2016) @@ -2003,7 +2703,9 @@ def test_rfc_feed(self): self.assertEqual(types, set(["text/plain", "text/html", "application/pdf"])) def test_state_help(self): - url = urlreverse('ietf.doc.views_help.state_help', kwargs=dict(type="draft-iesg")) + url = urlreverse( + "ietf.doc.views_help.state_help", kwargs=dict(type="draft-iesg") + ) r = self.client.get(url) self.assertEqual(r.status_code, 200) self.assertContains(r, State.objects.get(type="draft-iesg", slug="lc").name) @@ -2011,34 +2713,49 @@ def test_state_help(self): def test_document_nonietf_pubreq_button(self): doc = IndividualDraftFactory() - 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.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.assertNotContains(r, "Request publication") - 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))) + 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.assertContains(r, "Request publication") - 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))) + 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.assertNotContains(r, "Request publication") def _parse_bibtex_response(self, response) -> dict: parser = bibtexparser.bparser.BibTexParser(common_strings=True) parser.homogenise_fields = False # do not modify field names (e.g., turns "url" into "link" by default) - return bibtexparser.loads(response.content.decode(), parser=parser).get_entry_dict() + return bibtexparser.loads( + response.content.decode(), parser=parser + ).get_entry_dict() - @override_settings(RFC_EDITOR_INFO_BASE_URL='https://www.rfc-editor.ietf.org/info/') + @override_settings(RFC_EDITOR_INFO_BASE_URL="https://www.rfc-editor.ietf.org/info/") def test_document_bibtex(self): - for factory in [CharterFactory, BcpFactory, StatusChangeFactory, ConflictReviewFactory]: # Should be extended to all other doc types + for factory in [ + CharterFactory, + BcpFactory, + StatusChangeFactory, + ConflictReviewFactory, + ]: # Should be extended to all other doc types doc = factory() - url = urlreverse("ietf.doc.views_doc.document_bibtex", kwargs=dict(name=doc.name)) + url = urlreverse( + "ietf.doc.views_doc.document_bibtex", kwargs=dict(name=doc.name) + ) r = self.client.get(url) - self.assertEqual(r.status_code, 404) + self.assertEqual(r.status_code, 404) rfc = WgRfcFactory.create( time=datetime.datetime(2010, 10, 10, tzinfo=ZoneInfo(settings.TIME_ZONE)) ) @@ -2049,7 +2766,9 @@ def test_document_bibtex(self): time=datetime.datetime(2010, 10, 10, tzinfo=RPC_TZINFO), ) # - url = urlreverse("ietf.doc.views_doc.document_bibtex", kwargs=dict(name=rfc.name)) + url = urlreverse( + "ietf.doc.views_doc.document_bibtex", kwargs=dict(name=rfc.name) + ) r = self.client.get(url) entry = self._parse_bibtex_response(r)["rfc%s" % num] self.assertEqual(entry["series"], "Request for Comments") @@ -2060,7 +2779,7 @@ def test_document_bibtex(self): self.assertEqual(entry["url"], f"https://www.rfc-editor.ietf.org/info/rfc{num}") # self.assertNotIn("day", entry) - + # test for incorrect case - revision for RFC rfc = WgRfcFactory(name="rfc0000") url = urlreverse( @@ -2068,7 +2787,7 @@ def test_document_bibtex(self): ) r = self.client.get(url) self.assertEqual(r.status_code, 404) - + april1 = IndividualRfcFactory.create( stream_id="ise", std_level_id="inf", @@ -2094,11 +2813,13 @@ def test_document_bibtex(self): self.assertEqual(entry["month"].lower()[0:3], "apr") self.assertEqual(entry["day"], "1") self.assertEqual(entry["url"], f"https://www.rfc-editor.ietf.org/info/rfc{num}") - + draft = IndividualDraftFactory.create() docname = "%s-%s" % (draft.name, draft.rev) bibname = docname[6:] # drop the 'draft-' prefix - url = urlreverse("ietf.doc.views_doc.document_bibtex", kwargs=dict(name=draft.name)) + url = urlreverse( + "ietf.doc.views_doc.document_bibtex", kwargs=dict(name=draft.name) + ) r = self.client.get(url) entry = self._parse_bibtex_response(r)[bibname] self.assertEqual(entry["note"], "Work in Progress") @@ -2121,47 +2842,62 @@ def test_document_bibtex(self): def test_document_bibxml(self): draft = IndividualDraftFactory.create() - docname = '%s-%s' % (draft.name, draft.rev) - for viewname in [ 'ietf.doc.views_doc.document_bibxml', 'ietf.doc.views_doc.document_bibxml_ref' ]: + docname = "%s-%s" % (draft.name, draft.rev) + for viewname in [ + "ietf.doc.views_doc.document_bibxml", + "ietf.doc.views_doc.document_bibxml_ref", + ]: url = urlreverse(viewname, kwargs=dict(name=draft.name)) r = self.client.get(url) entry = lxml.etree.fromstring(r.content) - self.assertEqual(entry.find('./front/title').text, draft.title) - date = entry.find('./front/date') - self.assertEqual(date.get('year'), str(draft.pub_date().year)) - self.assertEqual(date.get('month'), draft.pub_date().strftime('%B')) - self.assertEqual(date.get('day'), str(draft.pub_date().day)) - self.assertEqual(normalize_text(entry.find('./front/abstract/t').text), normalize_text(draft.abstract)) - self.assertEqual(entry.find('./seriesInfo').get('value'), docname) + self.assertEqual(entry.find("./front/title").text, draft.title) + date = entry.find("./front/date") + self.assertEqual(date.get("year"), str(draft.pub_date().year)) + self.assertEqual(date.get("month"), draft.pub_date().strftime("%B")) + self.assertEqual(date.get("day"), str(draft.pub_date().day)) + self.assertEqual( + normalize_text(entry.find("./front/abstract/t").text), + normalize_text(draft.abstract), + ) + self.assertEqual(entry.find("./seriesInfo").get("value"), docname) self.assertEqual(entry.find('./seriesInfo[@name="DOI"]'), None) def test_trailing_hypen_digit_name_bibxml(self): - draft = WgDraftFactory(name='draft-ietf-mars-test-2') - docname = '%s-%s' % (draft.name, draft.rev) - for viewname in [ 'ietf.doc.views_doc.document_bibxml', 'ietf.doc.views_doc.document_bibxml_ref' ]: + draft = WgDraftFactory(name="draft-ietf-mars-test-2") + docname = "%s-%s" % (draft.name, draft.rev) + for viewname in [ + "ietf.doc.views_doc.document_bibxml", + "ietf.doc.views_doc.document_bibxml_ref", + ]: # This will need to be adjusted if settings.URL_REGEXPS is changed - url = urlreverse(viewname, kwargs=dict(name=draft.name[:-2], rev=draft.name[-1:]+'-'+draft.rev)) + url = urlreverse( + viewname, + kwargs=dict( + name=draft.name[:-2], rev=draft.name[-1:] + "-" + draft.rev + ), + ) r = self.client.get(url) entry = lxml.etree.fromstring(r.content) - self.assertEqual(entry.find('./front/title').text, draft.title) - self.assertEqual(entry.find('./seriesInfo').get('value'), docname) + self.assertEqual(entry.find("./front/title").text, draft.title) + self.assertEqual(entry.find("./seriesInfo").get("value"), docname) + class AddCommentTestCase(TestCase): def test_add_comment(self): - draft = WgDraftFactory(name='draft-ietf-mars-test',group__acronym='mars') - url = urlreverse('ietf.doc.views_doc.add_comment', kwargs=dict(name=draft.name)) + draft = WgDraftFactory(name="draft-ietf-mars-test", group__acronym="mars") + url = urlreverse("ietf.doc.views_doc.add_comment", kwargs=dict(name=draft.name)) login_testing_unauthorized(self, "secretary", url) # normal get r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(unicontent(r)) - self.assertEqual(len(q('form textarea[name=comment]')), 1) + self.assertEqual(len(q("form textarea[name=comment]")), 1) # request resurrect events_before = draft.docevent_set.count() mailbox_before = len(outbox) - + r = self.client.post(url, dict(comment="This is a test.")) self.assertEqual(r.status_code, 302) @@ -2169,9 +2905,9 @@ def test_add_comment(self): self.assertEqual("This is a test.", draft.latest_event().desc) self.assertEqual("added_comment", draft.latest_event().type) self.assertEqual(len(outbox), mailbox_before + 1) - self.assertIn("Comment added", outbox[-1]['Subject']) - self.assertIn(draft.name, outbox[-1]['Subject']) - self.assertIn('draft-ietf-mars-test@', outbox[-1]['To']) + self.assertIn("Comment added", outbox[-1]["Subject"]) + self.assertIn(draft.name, outbox[-1]["Subject"]) + self.assertIn("draft-ietf-mars-test@", outbox[-1]["To"]) # Make sure we can also do it as IANA self.client.login(username="iana", password="iana+password") @@ -2180,33 +2916,46 @@ def test_add_comment(self): r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(unicontent(r)) - self.assertEqual(len(q('form textarea[name=comment]')), 1) + self.assertEqual(len(q("form textarea[name=comment]")), 1) class TemplateTagTest(TestCase): def test_template_tags(self): import doctest from ietf.doc.templatetags import ietf_filters + failures, tests = doctest.testmod(ietf_filters) self.assertEqual(failures, 0) + class ReferencesTest(TestCase): def test_references(self): - doc1 = WgDraftFactory(name='draft-ietf-mars-test') - doc2 = IndividualDraftFactory(name='draft-imaginary-independent-submission') - RelatedDocument.objects.get_or_create(source=doc1,target=doc2,relationship=DocRelationshipName.objects.get(slug='refnorm')) - url = urlreverse('ietf.doc.views_doc.document_references', kwargs=dict(name=doc1.name)) + doc1 = WgDraftFactory(name="draft-ietf-mars-test") + doc2 = IndividualDraftFactory(name="draft-imaginary-independent-submission") + RelatedDocument.objects.get_or_create( + source=doc1, + target=doc2, + relationship=DocRelationshipName.objects.get(slug="refnorm"), + ) + url = urlreverse( + "ietf.doc.views_doc.document_references", kwargs=dict(name=doc1.name) + ) r = self.client.get(url) self.assertEqual(r.status_code, 200) self.assertContains(r, doc2.name) - url = urlreverse('ietf.doc.views_doc.document_referenced_by', kwargs=dict(name=doc2.name)) + url = urlreverse( + "ietf.doc.views_doc.document_referenced_by", kwargs=dict(name=doc2.name) + ) r = self.client.get(url) self.assertEqual(r.status_code, 200) self.assertContains(r, doc1.name) + class GenerateDraftAliasesTests(TestCase): - @override_settings(TOOLS_SERVER="tools.example.org", DRAFT_ALIAS_DOMAIN="draft.example.org") + @override_settings( + TOOLS_SERVER="tools.example.org", DRAFT_ALIAS_DOMAIN="draft.example.org" + ) def test_generator_class(self): """The DraftAliasGenerator should generate the same lists as the old mgmt cmd""" a_month_ago = (timezone.now() - datetime.timedelta(30)).astimezone(RPC_TZINFO) @@ -2226,7 +2975,9 @@ def test_generator_class(self): mars.role_set.create( name_id="chair", person=marschairman, email=marschairman.email() ) - doc1 = IndividualDraftFactory(authors=[author1], shepherd=shepherd.email(), ad=ad) + doc1 = IndividualDraftFactory( + authors=[author1], shepherd=shepherd.email(), ad=ad + ) doc2 = WgDraftFactory( name="draft-ietf-mars-test", group__acronym="mars", authors=[author2], ad=ad ) @@ -2306,7 +3057,12 @@ def test_generator_class(self): ) # check single name - output = [(alias, alist) for alias, alist in DraftAliasGenerator(Document.objects.filter(name=doc1.name))] + output = [ + (alias, alist) + for alias, alist in DraftAliasGenerator( + Document.objects.filter(name=doc1.name) + ) + ] alias_dict = dict(output) self.assertEqual(len(alias_dict), len(output)) # no duplicate aliases expected_dict = { @@ -2327,7 +3083,9 @@ def test_generator_class(self): {k: sorted(v) for k, v in expected_dict.items()}, ) - @override_settings(TOOLS_SERVER="tools.example.org", DRAFT_ALIAS_DOMAIN="draft.example.org") + @override_settings( + TOOLS_SERVER="tools.example.org", DRAFT_ALIAS_DOMAIN="draft.example.org" + ) def test_get_draft_notify_emails(self): ad = PersonFactory() shepherd = PersonFactory() @@ -2337,21 +3095,27 @@ def test_get_draft_notify_emails(self): doc.notify = f"{doc.name}@draft.example.org" doc.save() - self.assertCountEqual(generator.get_draft_notify_emails(doc), [author.email_address()]) + self.assertCountEqual( + generator.get_draft_notify_emails(doc), [author.email_address()] + ) doc.notify = f"{doc.name}.ad@draft.example.org" doc.save() - self.assertCountEqual(generator.get_draft_notify_emails(doc), [ad.email_address()]) + self.assertCountEqual( + generator.get_draft_notify_emails(doc), [ad.email_address()] + ) doc.notify = f"{doc.name}.shepherd@draft.example.org" doc.save() - self.assertCountEqual(generator.get_draft_notify_emails(doc), [shepherd.email_address()]) + self.assertCountEqual( + generator.get_draft_notify_emails(doc), [shepherd.email_address()] + ) doc.notify = f"{doc.name}.all@draft.example.org" doc.save() self.assertCountEqual( generator.get_draft_notify_emails(doc), - [ad.email_address(), author.email_address(), shepherd.email_address()] + [ad.email_address(), author.email_address(), shepherd.email_address()], ) doc.notify = f"{doc.name}.notify@draft.example.org" @@ -2360,13 +3124,18 @@ def test_get_draft_notify_emails(self): doc.notify = f"{doc.name}.ad@somewhere.example.com" doc.save() - self.assertCountEqual(generator.get_draft_notify_emails(doc), [f"{doc.name}.ad@somewhere.example.com"]) - - doc.notify = f"somebody@example.com, nobody@example.com, {doc.name}.ad@tools.example.org" + self.assertCountEqual( + generator.get_draft_notify_emails(doc), + [f"{doc.name}.ad@somewhere.example.com"], + ) + + doc.notify = ( + f"somebody@example.com, nobody@example.com, {doc.name}.ad@tools.example.org" + ) doc.save() self.assertCountEqual( generator.get_draft_notify_emails(doc), - ["somebody@example.com", "nobody@example.com", ad.email_address()] + ["somebody@example.com", "nobody@example.com", ad.email_address()], ) @@ -2374,52 +3143,120 @@ class EmailAliasesTests(TestCase): def setUp(self): super().setUp() - WgDraftFactory(name='draft-ietf-mars-test',group__acronym='mars') - WgDraftFactory(name='draft-ietf-ames-test',group__acronym='ames') - RoleFactory(group__type_id='review', group__acronym='yangdoctors', name_id='secr') - + WgDraftFactory(name="draft-ietf-mars-test", group__acronym="mars") + WgDraftFactory(name="draft-ietf-ames-test", group__acronym="ames") + RoleFactory( + group__type_id="review", group__acronym="yangdoctors", name_id="secr" + ) @mock.patch("ietf.doc.views_doc.get_doc_email_aliases") def testAliases(self, mock_get_aliases): mock_get_aliases.return_value = [ - {"doc_name": "draft-ietf-mars-test", "alias_type": "", "expansion": "mars-author@example.mars, mars-collaborator@example.mars"}, - {"doc_name": "draft-ietf-mars-test", "alias_type": ".authors", "expansion": "mars-author@example.mars, mars-collaborator@example.mars"}, - {"doc_name": "draft-ietf-mars-test", "alias_type": ".chairs", "expansion": "mars-chair@example.mars"}, - {"doc_name": "draft-ietf-mars-test", "alias_type": ".all", "expansion": "mars-author@example.mars, mars-collaborator@example.mars, mars-chair@example.mars"}, - {"doc_name": "draft-ietf-ames-test", "alias_type": "", "expansion": "ames-author@example.ames, ames-collaborator@example.ames"}, - {"doc_name": "draft-ietf-ames-test", "alias_type": ".authors", "expansion": "ames-author@example.ames, ames-collaborator@example.ames"}, - {"doc_name": "draft-ietf-ames-test", "alias_type": ".chairs", "expansion": "ames-chair@example.ames"}, - {"doc_name": "draft-ietf-ames-test", "alias_type": ".all", "expansion": "ames-author@example.ames, ames-collaborator@example.ames, ames-chair@example.ames"}, + { + "doc_name": "draft-ietf-mars-test", + "alias_type": "", + "expansion": "mars-author@example.mars, mars-collaborator@example.mars", + }, + { + "doc_name": "draft-ietf-mars-test", + "alias_type": ".authors", + "expansion": "mars-author@example.mars, mars-collaborator@example.mars", + }, + { + "doc_name": "draft-ietf-mars-test", + "alias_type": ".chairs", + "expansion": "mars-chair@example.mars", + }, + { + "doc_name": "draft-ietf-mars-test", + "alias_type": ".all", + "expansion": "mars-author@example.mars, mars-collaborator@example.mars, mars-chair@example.mars", + }, + { + "doc_name": "draft-ietf-ames-test", + "alias_type": "", + "expansion": "ames-author@example.ames, ames-collaborator@example.ames", + }, + { + "doc_name": "draft-ietf-ames-test", + "alias_type": ".authors", + "expansion": "ames-author@example.ames, ames-collaborator@example.ames", + }, + { + "doc_name": "draft-ietf-ames-test", + "alias_type": ".chairs", + "expansion": "ames-chair@example.ames", + }, + { + "doc_name": "draft-ietf-ames-test", + "alias_type": ".all", + "expansion": "ames-author@example.ames, ames-collaborator@example.ames, ames-chair@example.ames", + }, ] - PersonFactory(user__username='plain') - url = urlreverse('ietf.doc.urls.redirect.document_email', kwargs=dict(name="draft-ietf-mars-test")) + PersonFactory(user__username="plain") + url = urlreverse( + "ietf.doc.urls.redirect.document_email", + kwargs=dict(name="draft-ietf-mars-test"), + ) r = self.client.get(url) self.assertEqual(r.status_code, 302) - url = urlreverse('ietf.doc.views_doc.email_aliases', kwargs=dict()) + url = urlreverse("ietf.doc.views_doc.email_aliases", kwargs=dict()) login_testing_unauthorized(self, "plain", url) r = self.client.get(url) self.assertEqual(r.status_code, 200) self.assertEqual(mock_get_aliases.call_args, mock.call()) - self.assertTrue(all([x in unicontent(r) for x in ['mars-test@','mars-test.authors@','mars-test.chairs@']])) - self.assertTrue(all([x in unicontent(r) for x in ['ames-test@','ames-test.authors@','ames-test.chairs@']])) - + self.assertTrue( + all( + [ + x in unicontent(r) + for x in ["mars-test@", "mars-test.authors@", "mars-test.chairs@"] + ] + ) + ) + self.assertTrue( + all( + [ + x in unicontent(r) + for x in ["ames-test@", "ames-test.authors@", "ames-test.chairs@"] + ] + ) + ) @mock.patch("ietf.doc.views_doc.get_doc_email_aliases") def testExpansions(self, mock_get_aliases): mock_get_aliases.return_value = [ - {"doc_name": "draft-ietf-mars-test", "alias_type": "", "expansion": "mars-author@example.mars, mars-collaborator@example.mars"}, - {"doc_name": "draft-ietf-mars-test", "alias_type": ".authors", "expansion": "mars-author@example.mars, mars-collaborator@example.mars"}, - {"doc_name": "draft-ietf-mars-test", "alias_type": ".chairs", "expansion": "mars-chair@example.mars"}, - {"doc_name": "draft-ietf-mars-test", "alias_type": ".all", "expansion": "mars-author@example.mars, mars-collaborator@example.mars, mars-chair@example.mars"}, + { + "doc_name": "draft-ietf-mars-test", + "alias_type": "", + "expansion": "mars-author@example.mars, mars-collaborator@example.mars", + }, + { + "doc_name": "draft-ietf-mars-test", + "alias_type": ".authors", + "expansion": "mars-author@example.mars, mars-collaborator@example.mars", + }, + { + "doc_name": "draft-ietf-mars-test", + "alias_type": ".chairs", + "expansion": "mars-chair@example.mars", + }, + { + "doc_name": "draft-ietf-mars-test", + "alias_type": ".all", + "expansion": "mars-author@example.mars, mars-collaborator@example.mars, mars-chair@example.mars", + }, ] - url = urlreverse('ietf.doc.views_doc.document_email', kwargs=dict(name="draft-ietf-mars-test")) + url = urlreverse( + "ietf.doc.views_doc.document_email", + kwargs=dict(name="draft-ietf-mars-test"), + ) r = self.client.get(url) self.assertEqual(mock_get_aliases.call_args, mock.call("draft-ietf-mars-test")) self.assertEqual(r.status_code, 200) - self.assertContains(r, 'draft-ietf-mars-test.all@ietf.org') - self.assertContains(r, 'iesg_ballot_saved') - + self.assertContains(r, "draft-ietf-mars-test.all@ietf.org") + self.assertContains(r, "iesg_ballot_saved") + @mock.patch("ietf.doc.utils.DraftAliasGenerator") def test_get_doc_email_aliases(self, mock_alias_gen_cls): mock_alias_gen_cls.return_value = [ @@ -2461,73 +3298,132 @@ def test_get_doc_email_aliases(self, mock_alias_gen_cls): # check that the DraftAliasGenerator is called correctly draft = WgDraftFactory() get_doc_email_aliases(draft.name) - self.assertQuerySetEqual(mock_alias_gen_cls.call_args[0][0], Document.objects.filter(pk=draft.pk)) - - + self.assertQuerySetEqual( + mock_alias_gen_cls.call_args[0][0], Document.objects.filter(pk=draft.pk) + ) + + class DocumentMeetingTests(TestCase): def setUp(self): super().setUp() - self.group = GroupFactory(type_id='wg',state_id='active') + self.group = GroupFactory(type_id="wg", state_id="active") self.group_chair = PersonFactory() - self.group.role_set.create(name_id='chair',person=self.group_chair,email=self.group_chair.email()) + self.group.role_set.create( + name_id="chair", person=self.group_chair, email=self.group_chair.email() + ) - self.other_group = GroupFactory(type_id='wg',state_id='active') + self.other_group = GroupFactory(type_id="wg", state_id="active") self.other_chair = PersonFactory() - self.other_group.role_set.create(name_id='chair',person=self.other_chair,email=self.other_chair.email()) + self.other_group.role_set.create( + name_id="chair", person=self.other_chair, email=self.other_chair.email() + ) today = date_today() cut_days = settings.MEETING_MATERIALS_DEFAULT_SUBMISSION_CORRECTION_DAYS - self.past_cutoff = SessionFactory.create(meeting__type_id='ietf',group=self.group,meeting__date=today-datetime.timedelta(days=1+cut_days)) - self.past = SessionFactory.create(meeting__type_id='ietf',group=self.group,meeting__date=today-datetime.timedelta(days=cut_days/2)) - self.inprog = SessionFactory.create(meeting__type_id='ietf',group=self.group,meeting__date=today-datetime.timedelta(days=1)) - self.future = SessionFactory.create(meeting__type_id='ietf',group=self.group,meeting__date=today+datetime.timedelta(days=90)) - self.interim = SessionFactory.create(meeting__type_id='interim',group=self.group,meeting__date=today+datetime.timedelta(days=45)) + self.past_cutoff = SessionFactory.create( + meeting__type_id="ietf", + group=self.group, + meeting__date=today - datetime.timedelta(days=1 + cut_days), + ) + self.past = SessionFactory.create( + meeting__type_id="ietf", + group=self.group, + meeting__date=today - datetime.timedelta(days=cut_days / 2), + ) + self.inprog = SessionFactory.create( + meeting__type_id="ietf", + group=self.group, + meeting__date=today - datetime.timedelta(days=1), + ) + self.future = SessionFactory.create( + meeting__type_id="ietf", + group=self.group, + meeting__date=today + datetime.timedelta(days=90), + ) + self.interim = SessionFactory.create( + meeting__type_id="interim", + group=self.group, + meeting__date=today + datetime.timedelta(days=45), + ) def test_view_document_meetings(self): doc = IndividualDraftFactory.create() - doc.presentations.create(session=self.inprog,rev=None) - doc.presentations.create(session=self.interim,rev=None) + doc.presentations.create(session=self.inprog, rev=None) + doc.presentations.create(session=self.interim, rev=None) - url = urlreverse('ietf.doc.views_doc.all_presentations', kwargs=dict(name=doc.name)) + url = urlreverse( + "ietf.doc.views_doc.all_presentations", kwargs=dict(name=doc.name) + ) response = self.client.get(url) self.assertEqual(response.status_code, 200) q = PyQuery(response.content) - self.assertTrue(all([q(id) for id in ['#inprogressmeets','#futuremeets']])) - self.assertFalse(any([q(id) for id in ['#pastmeets',]])) - self.assertFalse(q('#addsessionsbutton')) + self.assertTrue(all([q(id) for id in ["#inprogressmeets", "#futuremeets"]])) + self.assertFalse( + any( + [ + q(id) + for id in [ + "#pastmeets", + ] + ] + ) + ) + self.assertFalse(q("#addsessionsbutton")) self.assertFalse(q("a.btn:contains('Remove document')")) - doc.presentations.create(session=self.past_cutoff,rev=None) - doc.presentations.create(session=self.past,rev=None) + doc.presentations.create(session=self.past_cutoff, rev=None) + doc.presentations.create(session=self.past, rev=None) self.client.login(username="secretary", password="secretary+password") response = self.client.get(url) self.assertEqual(response.status_code, 200) q = PyQuery(response.content) - self.assertTrue(q('#addsessionsbutton')) - self.assertEqual(1,len(q("#inprogressmeets a.btn-primary:contains('Remove document')"))) - self.assertEqual(1,len(q("#futuremeets a.btn-primary:contains('Remove document')"))) - self.assertEqual(1,len(q("#pastmeets a.btn-primary:contains('Remove document')"))) - self.assertEqual(1,len(q("#pastmeets a.btn-warning:contains('Remove document')"))) + self.assertTrue(q("#addsessionsbutton")) + self.assertEqual( + 1, len(q("#inprogressmeets a.btn-primary:contains('Remove document')")) + ) + self.assertEqual( + 1, len(q("#futuremeets a.btn-primary:contains('Remove document')")) + ) + self.assertEqual( + 1, len(q("#pastmeets a.btn-primary:contains('Remove document')")) + ) + self.assertEqual( + 1, len(q("#pastmeets a.btn-warning:contains('Remove document')")) + ) - self.client.login(username=self.group_chair.user.username,password='%s+password'%self.group_chair.user.username) + self.client.login( + username=self.group_chair.user.username, + password="%s+password" % self.group_chair.user.username, + ) response = self.client.get(url) self.assertEqual(response.status_code, 200) q = PyQuery(response.content) - self.assertTrue(q('#addsessionsbutton')) - self.assertEqual(1,len(q("#inprogressmeets a.btn-primary:contains('Remove document')"))) - self.assertEqual(1,len(q("#futuremeets a.btn-primary:contains('Remove document')"))) - self.assertEqual(1,len(q("#pastmeets a.btn-primary:contains('Remove document')"))) - self.assertTrue(q('#pastmeets')) + self.assertTrue(q("#addsessionsbutton")) + self.assertEqual( + 1, len(q("#inprogressmeets a.btn-primary:contains('Remove document')")) + ) + self.assertEqual( + 1, len(q("#futuremeets a.btn-primary:contains('Remove document')")) + ) + self.assertEqual( + 1, len(q("#pastmeets a.btn-primary:contains('Remove document')")) + ) + self.assertTrue(q("#pastmeets")) self.assertFalse(q("#pastmeets a.btn-warning:contains('Remove document')")) - self.client.login(username=self.other_chair.user.username,password='%s+password'%self.other_chair.user.username) + self.client.login( + username=self.other_chair.user.username, + password="%s+password" % self.other_chair.user.username, + ) response = self.client.get(url) self.assertEqual(response.status_code, 200) q = PyQuery(response.content) - self.assertTrue(q('#addsessionsbutton')) - self.assertTrue(all([q(id) for id in ['#futuremeets','#pastmeets','#inprogressmeets']])) + self.assertTrue(q("#addsessionsbutton")) + self.assertTrue( + all([q(id) for id in ["#futuremeets", "#pastmeets", "#inprogressmeets"]]) + ) self.assertFalse(q("#inprogressmeets a.btn:contains('Remove document')")) self.assertFalse(q("#futuremeets a.btn:contains('Remove document')")) self.assertFalse(q("#pastmeets a.btn:contains('Remove document')")) @@ -2536,41 +3432,56 @@ def test_view_document_meetings(self): @mock.patch("ietf.doc.views_doc.SlidesManager") def test_edit_document_session(self, mock_slides_manager_cls): doc = IndividualDraftFactory.create() - sp = doc.presentations.create(session=self.future,rev=None) + sp = doc.presentations.create(session=self.future, rev=None) - url = urlreverse('ietf.doc.views_doc.edit_sessionpresentation',kwargs=dict(name='no-such-doc',session_id=sp.session_id)) + url = urlreverse( + "ietf.doc.views_doc.edit_sessionpresentation", + kwargs=dict(name="no-such-doc", session_id=sp.session_id), + ) response = self.client.get(url) self.assertEqual(response.status_code, 404) self.assertFalse(mock_slides_manager_cls.called) - url = urlreverse('ietf.doc.views_doc.edit_sessionpresentation',kwargs=dict(name=doc.name,session_id=0)) + url = urlreverse( + "ietf.doc.views_doc.edit_sessionpresentation", + kwargs=dict(name=doc.name, session_id=0), + ) response = self.client.get(url) self.assertEqual(response.status_code, 404) self.assertFalse(mock_slides_manager_cls.called) - url = urlreverse('ietf.doc.views_doc.edit_sessionpresentation',kwargs=dict(name=doc.name,session_id=sp.session_id)) + url = urlreverse( + "ietf.doc.views_doc.edit_sessionpresentation", + kwargs=dict(name=doc.name, session_id=sp.session_id), + ) response = self.client.get(url) self.assertEqual(response.status_code, 404) self.assertFalse(mock_slides_manager_cls.called) - self.client.login(username=self.other_chair.user.username,password='%s+password'%self.other_chair.user.username) + self.client.login( + username=self.other_chair.user.username, + password="%s+password" % self.other_chair.user.username, + ) response = self.client.get(url) self.assertEqual(response.status_code, 404) self.assertFalse(mock_slides_manager_cls.called) - self.client.login(username=self.group_chair.user.username,password='%s+password'%self.group_chair.user.username) + self.client.login( + username=self.group_chair.user.username, + password="%s+password" % self.group_chair.user.username, + ) response = self.client.get(url) self.assertEqual(response.status_code, 200) q = PyQuery(response.content) - self.assertEqual(2,len(q('select#id_version option'))) + self.assertEqual(2, len(q("select#id_version option"))) self.assertFalse(mock_slides_manager_cls.called) # edit draft - self.assertEqual(1,doc.docevent_set.count()) - response = self.client.post(url,{'version':'00','save':''}) + self.assertEqual(1, doc.docevent_set.count()) + response = self.client.post(url, {"version": "00", "save": ""}) self.assertEqual(response.status_code, 302) - self.assertEqual(doc.presentations.get(pk=sp.pk).rev,'00') - self.assertEqual(2,doc.docevent_set.count()) + self.assertEqual(doc.presentations.get(pk=sp.pk).rev, "00") + self.assertEqual(2, doc.docevent_set.count()) self.assertFalse(mock_slides_manager_cls.called) # editing slides should call Meetecho API @@ -2588,7 +3499,9 @@ def test_edit_document_session(self, mock_slides_manager_cls): response = self.client.post(url, {"version": "00", "save": ""}) self.assertEqual(response.status_code, 302) self.assertEqual(mock_slides_manager_cls.call_count, 1) - self.assertEqual(mock_slides_manager_cls.call_args, mock.call(api_config="fake settings")) + self.assertEqual( + mock_slides_manager_cls.call_args, mock.call(api_config="fake settings") + ) self.assertEqual(mock_slides_manager_cls.return_value.send_update.call_count, 1) self.assertEqual( mock_slides_manager_cls.return_value.send_update.call_args, @@ -2597,60 +3510,85 @@ def test_edit_document_session(self, mock_slides_manager_cls): def test_edit_document_session_after_proceedings_closed(self): doc = IndividualDraftFactory.create() - sp = doc.presentations.create(session=self.past_cutoff,rev=None) + sp = doc.presentations.create(session=self.past_cutoff, rev=None) - url = urlreverse('ietf.doc.views_doc.edit_sessionpresentation',kwargs=dict(name=doc.name,session_id=sp.session_id)) - self.client.login(username=self.group_chair.user.username,password='%s+password'%self.group_chair.user.username) + url = urlreverse( + "ietf.doc.views_doc.edit_sessionpresentation", + kwargs=dict(name=doc.name, session_id=sp.session_id), + ) + self.client.login( + username=self.group_chair.user.username, + password="%s+password" % self.group_chair.user.username, + ) response = self.client.get(url) self.assertEqual(response.status_code, 404) - - self.client.login(username='secretary',password='secretary+password') + + self.client.login(username="secretary", password="secretary+password") response = self.client.get(url) self.assertEqual(response.status_code, 200) - q=PyQuery(response.content) - self.assertEqual(1,len(q(".alert-warning:contains('may affect published proceedings')"))) + q = PyQuery(response.content) + self.assertEqual( + 1, len(q(".alert-warning:contains('may affect published proceedings')")) + ) @override_settings(MEETECHO_API_CONFIG="fake settings") @mock.patch("ietf.doc.views_doc.SlidesManager") def test_remove_document_session(self, mock_slides_manager_cls): doc = IndividualDraftFactory.create() - sp = doc.presentations.create(session=self.future,rev=None) + sp = doc.presentations.create(session=self.future, rev=None) - url = urlreverse('ietf.doc.views_doc.remove_sessionpresentation',kwargs=dict(name='no-such-doc',session_id=sp.session_id)) + url = urlreverse( + "ietf.doc.views_doc.remove_sessionpresentation", + kwargs=dict(name="no-such-doc", session_id=sp.session_id), + ) response = self.client.get(url) self.assertEqual(response.status_code, 404) self.assertFalse(mock_slides_manager_cls.called) - url = urlreverse('ietf.doc.views_doc.remove_sessionpresentation',kwargs=dict(name=doc.name,session_id=0)) + url = urlreverse( + "ietf.doc.views_doc.remove_sessionpresentation", + kwargs=dict(name=doc.name, session_id=0), + ) response = self.client.get(url) self.assertEqual(response.status_code, 404) self.assertFalse(mock_slides_manager_cls.called) - url = urlreverse('ietf.doc.views_doc.remove_sessionpresentation',kwargs=dict(name=doc.name,session_id=sp.session_id)) + url = urlreverse( + "ietf.doc.views_doc.remove_sessionpresentation", + kwargs=dict(name=doc.name, session_id=sp.session_id), + ) response = self.client.get(url) self.assertEqual(response.status_code, 404) self.assertFalse(mock_slides_manager_cls.called) - self.client.login(username=self.other_chair.user.username,password='%s+password'%self.other_chair.user.username) + self.client.login( + username=self.other_chair.user.username, + password="%s+password" % self.other_chair.user.username, + ) response = self.client.get(url) self.assertEqual(response.status_code, 404) self.assertFalse(mock_slides_manager_cls.called) - self.client.login(username=self.group_chair.user.username,password='%s+password'%self.group_chair.user.username) + self.client.login( + username=self.group_chair.user.username, + password="%s+password" % self.group_chair.user.username, + ) response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertFalse(mock_slides_manager_cls.called) # removing a draft - self.assertEqual(1,doc.docevent_set.count()) - response = self.client.post(url,{'remove_session':''}) + self.assertEqual(1, doc.docevent_set.count()) + response = self.client.post(url, {"remove_session": ""}) self.assertEqual(response.status_code, 302) self.assertFalse(doc.presentations.filter(pk=sp.pk).exists()) - self.assertEqual(2,doc.docevent_set.count()) + self.assertEqual(2, doc.docevent_set.count()) self.assertFalse(mock_slides_manager_cls.called) # removing slides should call Meetecho API - slides = SessionPresentationFactory(session=self.future, document__type_id="slides", order=1).document + slides = SessionPresentationFactory( + session=self.future, document__type_id="slides", order=1 + ).document url = urlreverse( "ietf.doc.views_doc.remove_sessionpresentation", kwargs={"name": slides.name, "session_id": self.future.pk}, @@ -2658,7 +3596,9 @@ def test_remove_document_session(self, mock_slides_manager_cls): response = self.client.post(url, {"remove_session": ""}) self.assertEqual(response.status_code, 302) self.assertEqual(mock_slides_manager_cls.call_count, 1) - self.assertEqual(mock_slides_manager_cls.call_args, mock.call(api_config="fake settings")) + self.assertEqual( + mock_slides_manager_cls.call_args, mock.call(api_config="fake settings") + ) self.assertEqual(mock_slides_manager_cls.return_value.delete.call_count, 1) self.assertEqual( mock_slides_manager_cls.return_value.delete.call_args, @@ -2667,57 +3607,77 @@ def test_remove_document_session(self, mock_slides_manager_cls): def test_remove_document_session_after_proceedings_closed(self): doc = IndividualDraftFactory.create() - sp = doc.presentations.create(session=self.past_cutoff,rev=None) + sp = doc.presentations.create(session=self.past_cutoff, rev=None) - url = urlreverse('ietf.doc.views_doc.remove_sessionpresentation',kwargs=dict(name=doc.name,session_id=sp.session_id)) - self.client.login(username=self.group_chair.user.username,password='%s+password'%self.group_chair.user.username) + url = urlreverse( + "ietf.doc.views_doc.remove_sessionpresentation", + kwargs=dict(name=doc.name, session_id=sp.session_id), + ) + self.client.login( + username=self.group_chair.user.username, + password="%s+password" % self.group_chair.user.username, + ) response = self.client.get(url) self.assertEqual(response.status_code, 404) - - self.client.login(username='secretary',password='secretary+password') + + self.client.login(username="secretary", password="secretary+password") response = self.client.get(url) self.assertEqual(response.status_code, 200) - q=PyQuery(response.content) - self.assertEqual(1,len(q(".alert-warning:contains('may affect published proceedings')"))) + q = PyQuery(response.content) + self.assertEqual( + 1, len(q(".alert-warning:contains('may affect published proceedings')")) + ) @override_settings(MEETECHO_API_CONFIG="fake settings") @mock.patch("ietf.doc.views_doc.SlidesManager") def test_add_document_session(self, mock_slides_manager_cls): doc = IndividualDraftFactory.create() - url = urlreverse('ietf.doc.views_doc.add_sessionpresentation',kwargs=dict(name=doc.name)) - login_testing_unauthorized(self,self.group_chair.user.username,url) + url = urlreverse( + "ietf.doc.views_doc.add_sessionpresentation", kwargs=dict(name=doc.name) + ) + login_testing_unauthorized(self, self.group_chair.user.username, url) response = self.client.get(url) - self.assertEqual(response.status_code,200) + self.assertEqual(response.status_code, 200) self.assertFalse(mock_slides_manager_cls.called) - response = self.client.post(url,{'session':0,'version':'current'}) - self.assertEqual(response.status_code,200) - q=PyQuery(response.content) - self.assertTrue(q('.form-select.is-invalid')) + response = self.client.post(url, {"session": 0, "version": "current"}) + self.assertEqual(response.status_code, 200) + q = PyQuery(response.content) + self.assertTrue(q(".form-select.is-invalid")) self.assertFalse(mock_slides_manager_cls.called) - response = self.client.post(url,{'session':self.future.pk,'version':'bogus version'}) - self.assertEqual(response.status_code,200) - q=PyQuery(response.content) - self.assertTrue(q('.form-select.is-invalid')) + response = self.client.post( + url, {"session": self.future.pk, "version": "bogus version"} + ) + self.assertEqual(response.status_code, 200) + q = PyQuery(response.content) + self.assertTrue(q(".form-select.is-invalid")) self.assertFalse(mock_slides_manager_cls.called) # adding a draft - self.assertEqual(1,doc.docevent_set.count()) - response = self.client.post(url,{'session':self.future.pk,'version':'current'}) - self.assertEqual(response.status_code,302) - self.assertEqual(2,doc.docevent_set.count()) + self.assertEqual(1, doc.docevent_set.count()) + response = self.client.post( + url, {"session": self.future.pk, "version": "current"} + ) + self.assertEqual(response.status_code, 302) + self.assertEqual(2, doc.docevent_set.count()) self.assertEqual(doc.presentations.get(session__pk=self.future.pk).order, 0) self.assertFalse(mock_slides_manager_cls.called) # adding slides should set order / call Meetecho API slides = DocumentFactory(type_id="slides") - url = urlreverse("ietf.doc.views_doc.add_sessionpresentation", kwargs=dict(name=slides.name)) - response = self.client.post(url, {"session": self.future.pk, "version": "current"}) - self.assertEqual(response.status_code,302) + url = urlreverse( + "ietf.doc.views_doc.add_sessionpresentation", kwargs=dict(name=slides.name) + ) + response = self.client.post( + url, {"session": self.future.pk, "version": "current"} + ) + self.assertEqual(response.status_code, 302) self.assertEqual(slides.presentations.get(session__pk=self.future.pk).order, 1) - self.assertEqual(mock_slides_manager_cls.call_args, mock.call(api_config="fake settings")) + self.assertEqual( + mock_slides_manager_cls.call_args, mock.call(api_config="fake settings") + ) self.assertEqual(mock_slides_manager_cls.return_value.add.call_count, 1) self.assertEqual( mock_slides_manager_cls.return_value.add.call_args, @@ -2726,62 +3686,100 @@ def test_add_document_session(self, mock_slides_manager_cls): def test_get_related_meeting(self): """Should be able to retrieve related meeting""" - meeting = MeetingFactory(type_id='ietf') + meeting = MeetingFactory(type_id="ietf") session = SessionFactory(meeting=meeting) procmat = ProceedingsMaterialFactory(meeting=meeting) for doctype in DocTypeName.objects.filter(used=True): doc = DocumentFactory(type=doctype) - self.assertIsNone(doc.get_related_meeting(), 'Doc does not yet have a connection to the meeting') + self.assertIsNone( + doc.get_related_meeting(), + "Doc does not yet have a connection to the meeting", + ) # test through a session doc.session_set.add(session) doc = Document.objects.get(pk=doc.pk) if doc.meeting_related(): - self.assertEqual(doc.get_related_meeting(), meeting, f'{doc.type.slug} should be related to meeting') + self.assertEqual( + doc.get_related_meeting(), + meeting, + f"{doc.type.slug} should be related to meeting", + ) else: - self.assertIsNone(doc.get_related_meeting(), f'{doc.type.slug} should not be related to meeting') + self.assertIsNone( + doc.get_related_meeting(), + f"{doc.type.slug} should not be related to meeting", + ) # test with both session and procmat doc.proceedingsmaterial_set.add(procmat) doc = Document.objects.get(pk=doc.pk) if doc.meeting_related(): - self.assertEqual(doc.get_related_meeting(), meeting, f'{doc.type.slug} should be related to meeting') + self.assertEqual( + doc.get_related_meeting(), + meeting, + f"{doc.type.slug} should be related to meeting", + ) else: - self.assertIsNone(doc.get_related_meeting(), f'{doc.type.slug} should not be related to meeting') + self.assertIsNone( + doc.get_related_meeting(), + f"{doc.type.slug} should not be related to meeting", + ) # and test with only procmat doc.session_set.remove(session) doc = Document.objects.get(pk=doc.pk) if doc.meeting_related(): - self.assertEqual(doc.get_related_meeting(), meeting, f'{doc.type.slug} should be related to meeting') + self.assertEqual( + doc.get_related_meeting(), + meeting, + f"{doc.type.slug} should be related to meeting", + ) else: - self.assertIsNone(doc.get_related_meeting(), f'{doc.type.slug} should not be related to meeting') + self.assertIsNone( + doc.get_related_meeting(), + f"{doc.type.slug} should not be related to meeting", + ) + class ChartTests(ResourceTestCaseMixin, TestCase): def test_personal_chart(self): person = PersonFactory.create() IndividualDraftFactory.create( - authors=[person, ], + authors=[ + person, + ], ) - conf_url = urlreverse('ietf.doc.views_stats.chart_conf_person_drafts', kwargs=dict(id=person.id)) + conf_url = urlreverse( + "ietf.doc.views_stats.chart_conf_person_drafts", kwargs=dict(id=person.id) + ) r = self.client.get(conf_url) self.assertValidJSONResponse(r) d = r.json() - self.assertEqual(d['chart']['type'], settings.CHART_TYPE_COLUMN_OPTIONS['chart']['type']) - self.assertEqual("New Internet-Draft revisions over time for %s" % person.name, d['title']['text']) + self.assertEqual( + d["chart"]["type"], settings.CHART_TYPE_COLUMN_OPTIONS["chart"]["type"] + ) + self.assertEqual( + "New Internet-Draft revisions over time for %s" % person.name, + d["title"]["text"], + ) - data_url = urlreverse('ietf.doc.views_stats.chart_data_person_drafts', kwargs=dict(id=person.id)) + data_url = urlreverse( + "ietf.doc.views_stats.chart_data_person_drafts", kwargs=dict(id=person.id) + ) r = self.client.get(data_url) self.assertValidJSONResponse(r) d = r.json() self.assertEqual(len(d), 1) self.assertEqual(len(d[0]), 2) - self.assertEqual(d[0][1], 1) + self.assertEqual(d[0][1], 1) - page_url = urlreverse('ietf.person.views.profile', kwargs=dict(email_or_name=person.name)) + page_url = urlreverse( + "ietf.person.views.profile", kwargs=dict(email_or_name=person.name) + ) r = self.client.get(page_url) self.assertEqual(r.status_code, 200) - + class FieldTests(TestCase): def test_searchabledocumentsfield_pre(self): @@ -2790,147 +3788,195 @@ def test_searchabledocumentsfield_pre(self): class _TestForm(Form): test_field = SearchableDocumentsField() - + form = _TestForm(initial=dict(test_field=docs)) html = str(form) q = PyQuery(html) - json_data = q('.select2-field').attr('data-pre') + json_data = q(".select2-field").attr("data-pre") try: decoded = json.loads(json_data) except json.JSONDecodeError as e: - self.fail('data-pre contained invalid JSON data: %s' % str(e)) - decoded_ids = [item['id'] for item in decoded] + self.fail("data-pre contained invalid JSON data: %s" % str(e)) + decoded_ids = [item["id"] for item in decoded] self.assertEqual(decoded_ids, [doc.id for doc in docs]) for doc in docs: self.assertEqual( - dict(id=doc.pk, selected=True, url=doc.get_absolute_url(), text=escape(uppercase_std_abbreviated_name(doc.name))), + dict( + id=doc.pk, + selected=True, + url=doc.get_absolute_url(), + text=escape(uppercase_std_abbreviated_name(doc.name)), + ), decoded[decoded_ids.index(doc.pk)], ) + class MaterialsTests(TestCase): - settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ['AGENDA_PATH'] + settings_temp_path_overrides = TestCase.settings_temp_path_overrides + [ + "AGENDA_PATH" + ] + def setUp(self): super().setUp() - meeting_number='111' + meeting_number = "111" meeting_dir = Path(settings.AGENDA_PATH) / meeting_number meeting_dir.mkdir() - agenda_dir = meeting_dir / 'agenda' + agenda_dir = meeting_dir / "agenda" agenda_dir.mkdir() - group_acronym='bogons' + group_acronym = "bogons" - # This is too much work - the factory should - # * build the DocumentHistory correctly + # This is too much work - the factory should + # * build the DocumentHistory correctly # * maybe do something by default with uploaded_filename # and there should be a more usable unit to save bits to disk (handle_file_upload isn't quite right) that tests can leverage - uploaded_filename_00 = f'agenda-{meeting_number}-{group_acronym}-00.txt' - uploaded_filename_01 = f'agenda-{meeting_number}-{group_acronym}-01.md' - f = io.open(os.path.join(agenda_dir, uploaded_filename_00), 'w') - f.write('This is some unremarkable text') + uploaded_filename_00 = f"agenda-{meeting_number}-{group_acronym}-00.txt" + uploaded_filename_01 = f"agenda-{meeting_number}-{group_acronym}-01.md" + f = io.open(os.path.join(agenda_dir, uploaded_filename_00), "w") + f.write("This is some unremarkable text") f.close() - f = io.open(os.path.join(agenda_dir, uploaded_filename_01), 'w') - f.write('This links to [an unusual place](https://unusual.example).') + f = io.open(os.path.join(agenda_dir, uploaded_filename_01), "w") + f.write("This links to [an unusual place](https://unusual.example).") f.close() - self.doc = DocumentFactory(type_id='agenda',rev='00',group__acronym=group_acronym, newrevisiondocevent=None, name=f'agenda-{meeting_number}-{group_acronym}', uploaded_filename=uploaded_filename_00) - e = NewRevisionDocEventFactory(doc=self.doc,rev='00') + self.doc = DocumentFactory( + type_id="agenda", + rev="00", + group__acronym=group_acronym, + newrevisiondocevent=None, + name=f"agenda-{meeting_number}-{group_acronym}", + uploaded_filename=uploaded_filename_00, + ) + e = NewRevisionDocEventFactory(doc=self.doc, rev="00") self.doc.save_with_history([e]) - self.doc.rev = '01' + self.doc.rev = "01" self.doc.uploaded_filename = uploaded_filename_01 - e = NewRevisionDocEventFactory(doc=self.doc, rev='01') + e = NewRevisionDocEventFactory(doc=self.doc, rev="01") self.doc.save_with_history([e]) # This is necessary for the view to be able to find the document # which hints that the view has an issue : if a materials document is taken out of all SessionPresentations, it is no longer accessible by this view - SessionPresentationFactory(session__meeting__number=meeting_number, session__group=self.doc.group, document=self.doc) + SessionPresentationFactory( + session__meeting__number=meeting_number, + session__group=self.doc.group, + document=self.doc, + ) def test_markdown_and_text(self): - url = urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=self.doc.name,rev='00')) + url = urlreverse( + "ietf.doc.views_doc.document_main", + kwargs=dict(name=self.doc.name, rev="00"), + ) r = self.client.get(url) - self.assertEqual(r.status_code,200) + self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertTrue(q('#materials-content pre')) + self.assertTrue(q("#materials-content pre")) - url = urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=self.doc.name,rev='01')) + url = urlreverse( + "ietf.doc.views_doc.document_main", + kwargs=dict(name=self.doc.name, rev="01"), + ) r = self.client.get(url) - self.assertEqual(r.status_code,200) + self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertEqual(q('#materials-content .card-body a').attr['href'],'https://unusual.example') + self.assertEqual( + q("#materials-content .card-body a").attr["href"], "https://unusual.example" + ) + class Idnits2SupportTests(TestCase): - settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ['DERIVED_DIR'] + settings_temp_path_overrides = TestCase.settings_temp_path_overrides + [ + "DERIVED_DIR" + ] def test_generate_idnits2_rfcs_obsoleted(self): rfc = WgRfcFactory(rfc_number=1001) - WgRfcFactory(rfc_number=1003,relations=[('obs',rfc)]) + WgRfcFactory(rfc_number=1003, relations=[("obs", rfc)]) rfc = WgRfcFactory(rfc_number=1005) - WgRfcFactory(rfc_number=1007,relations=[('obs',rfc)]) + WgRfcFactory(rfc_number=1007, relations=[("obs", rfc)]) blob = generate_idnits2_rfcs_obsoleted() - self.assertEqual(blob, b'1001 1003\n1005 1007\n'.decode("utf8")) + self.assertEqual(blob, b"1001 1003\n1005 1007\n".decode("utf8")) def test_obsoleted(self): - url = urlreverse('ietf.doc.views_doc.idnits2_rfcs_obsoleted') + url = urlreverse("ietf.doc.views_doc.idnits2_rfcs_obsoleted") r = self.client.get(url) self.assertEqual(r.status_code, 404) # value written is arbitrary, expect it to be passed through - (Path(settings.DERIVED_DIR) / "idnits2-rfcs-obsoleted").write_bytes(b'1001 1003\n1005 1007\n') - url = urlreverse('ietf.doc.views_doc.idnits2_rfcs_obsoleted') + (Path(settings.DERIVED_DIR) / "idnits2-rfcs-obsoleted").write_bytes( + b"1001 1003\n1005 1007\n" + ) + url = urlreverse("ietf.doc.views_doc.idnits2_rfcs_obsoleted") r = self.client.get(url) self.assertEqual(r.status_code, 200) - self.assertEqual(r.content, b'1001 1003\n1005 1007\n') + self.assertEqual(r.content, b"1001 1003\n1005 1007\n") def test_generate_idnits2_rfc_status(self): - for slug in ('bcp', 'ds', 'exp', 'hist', 'inf', 'std', 'ps', 'unkn'): + for slug in ("bcp", "ds", "exp", "hist", "inf", "std", "ps", "unkn"): WgRfcFactory(std_level_id=slug) blob = generate_idnits2_rfc_status().replace("\n", "") - self.assertEqual(blob[6312-1], "O") + self.assertEqual(blob[6312 - 1], "O") def test_rfc_status(self): - url = urlreverse('ietf.doc.views_doc.idnits2_rfc_status') + url = urlreverse("ietf.doc.views_doc.idnits2_rfc_status") r = self.client.get(url) - self.assertEqual(r.status_code,404) + self.assertEqual(r.status_code, 404) # value written is arbitrary, expect it to be passed through - (Path(settings.DERIVED_DIR) / "idnits2-rfc-status").write_bytes(b'1001 1003\n1005 1007\n') + (Path(settings.DERIVED_DIR) / "idnits2-rfc-status").write_bytes( + b"1001 1003\n1005 1007\n" + ) r = self.client.get(url) - self.assertEqual(r.status_code,200) - self.assertEqual(r.content, b'1001 1003\n1005 1007\n') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.content, b"1001 1003\n1005 1007\n") def test_idnits2_state(self): rfc = WgRfcFactory() draft = WgDraftFactory() draft.relateddocument_set.create(relationship_id="became_rfc", target=rfc) - url = urlreverse('ietf.doc.views_doc.idnits2_state', kwargs=dict(name=rfc.name)) + url = urlreverse("ietf.doc.views_doc.idnits2_state", kwargs=dict(name=rfc.name)) r = self.client.get(url) self.assertEqual(r.status_code, 200) - self.assertContains(r,'rfcnum') + self.assertContains(r, "rfcnum") draft = WgDraftFactory() - url = urlreverse('ietf.doc.views_doc.idnits2_state', kwargs=dict(name=draft.name)) + url = urlreverse( + "ietf.doc.views_doc.idnits2_state", kwargs=dict(name=draft.name) + ) r = self.client.get(url) self.assertEqual(r.status_code, 200) - self.assertNotContains(r,'rfcnum') - self.assertContains(r,'Unknown') + self.assertNotContains(r, "rfcnum") + self.assertContains(r, "Unknown") - draft = WgDraftFactory(intended_std_level_id='ps') - url = urlreverse('ietf.doc.views_doc.idnits2_state', kwargs=dict(name=draft.name)) + draft = WgDraftFactory(intended_std_level_id="ps") + url = urlreverse( + "ietf.doc.views_doc.idnits2_state", kwargs=dict(name=draft.name) + ) r = self.client.get(url) self.assertEqual(r.status_code, 200) - self.assertContains(r,'Proposed') + self.assertContains(r, "Proposed") class RawIdTests(TestCase): def __init__(self, *args, **kwargs): self.view = "ietf.doc.views_doc.document_raw_id" - self.mimetypes = {'txt':'text/plain','html':'text/html','xml':'application/xml'} + self.mimetypes = { + "txt": "text/plain", + "html": "text/html", + "xml": "application/xml", + } super(self.__class__, self).__init__(*args, **kwargs) def should_succeed(self, argdict): url = urlreverse(self.view, kwargs=argdict) - r = self.client.get(url, skip_verify=True) # do not verify HTML, they're faked anyway - self.assertEqual(r.status_code,200) - self.assertEqual(r.get('Content-Type'),f"{self.mimetypes[argdict.get('ext','txt')]};charset=utf-8") + r = self.client.get( + url, skip_verify=True + ) # do not verify HTML, they're faked anyway + self.assertEqual(r.status_code, 200) + self.assertEqual( + r.get("Content-Type"), + f"{self.mimetypes[argdict.get('ext','txt')]};charset=utf-8", + ) def should_404(self, argdict): url = urlreverse(self.view, kwargs=argdict) @@ -2938,25 +3984,25 @@ def should_404(self, argdict): self.assertEqual(r.status_code, 404) def test_raw_id(self): - draft = WgDraftFactory(create_revisions=range(0,2)) + draft = WgDraftFactory(create_revisions=range(0, 2)) dir = settings.INTERNET_ALL_DRAFTS_ARCHIVE_DIR - for r in range(0,2): - rev = f'{r:02d}' - (Path(dir) / f'{draft.name}-{rev}.txt').touch() + for r in range(0, 2): + rev = f"{r:02d}" + (Path(dir) / f"{draft.name}-{rev}.txt").touch() if r == 1: - (Path(dir) / f'{draft.name}-{rev}.html').touch() - (Path(dir) / f'{draft.name}-{rev}.xml').touch() + (Path(dir) / f"{draft.name}-{rev}.html").touch() + (Path(dir) / f"{draft.name}-{rev}.xml").touch() self.should_succeed(dict(name=draft.name)) - for ext in ('txt', 'html', 'xml'): + for ext in ("txt", "html", "xml"): self.should_succeed(dict(name=draft.name, ext=ext)) - self.should_succeed(dict(name=draft.name, rev='01', ext=ext)) - self.should_404(dict(name=draft.name, ext='pdf')) + self.should_succeed(dict(name=draft.name, rev="01", ext=ext)) + self.should_404(dict(name=draft.name, ext="pdf")) - self.should_succeed(dict(name=draft.name, rev='00')) - self.should_succeed(dict(name=draft.name, rev='00',ext='txt')) - self.should_404(dict(name=draft.name, rev='00',ext='html')) + self.should_succeed(dict(name=draft.name, rev="00")) + self.should_succeed(dict(name=draft.name, rev="00", ext="txt")) + self.should_404(dict(name=draft.name, rev="00", ext="html")) # test_raw_id_rfc intentionally removed # an rfc is no longer a pseudo-version of a draft. @@ -2965,6 +4011,7 @@ def test_non_draft(self): for doc in [CharterFactory(), WgRfcFactory()]: self.should_404(dict(name=doc.name)) + class PdfizedTests(TestCase): def __init__(self, *args, **kwargs): @@ -2974,8 +4021,8 @@ def __init__(self, *args, **kwargs): def should_succeed(self, argdict): url = urlreverse(self.view, kwargs=argdict) r = self.client.get(url) - self.assertEqual(r.status_code,200) - self.assertEqual(r.get('Content-Type'),'application/pdf') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.get("Content-Type"), "application/pdf") def should_404(self, argdict): url = urlreverse(self.view, kwargs=argdict) @@ -2985,16 +4032,16 @@ def should_404(self, argdict): # This takes a _long_ time (32s on a 2022 m1 macbook pro) - is it worth what it covers? def test_pdfized(self): rfc = WgRfcFactory() - draft = WgDraftFactory(create_revisions=range(0,2)) + draft = WgDraftFactory(create_revisions=range(0, 2)) draft.relateddocument_set.create(relationship_id="became_rfc", target=rfc) dir = settings.RFC_PATH - with (Path(dir) / f'{rfc.name}.txt').open('w') as f: - f.write('text content') + with (Path(dir) / f"{rfc.name}.txt").open("w") as f: + f.write("text content") dir = settings.INTERNET_ALL_DRAFTS_ARCHIVE_DIR - for r in range(0,2): - with (Path(dir) / f'{draft.name}-{r:02d}.txt').open('w') as f: - f.write('text content') + for r in range(0, 2): + with (Path(dir) / f"{draft.name}-{r:02d}.txt").open("w") as f: + f.write("text content") self.assertTrue( login_testing_unauthorized( @@ -3005,25 +4052,28 @@ def test_pdfized(self): ) self.should_succeed(dict(name=rfc.name)) self.should_succeed(dict(name=draft.name)) - for r in range(0,2): - self.should_succeed(dict(name=draft.name,rev=f'{r:02d}')) - for ext in ('pdf','txt','html','anythingatall'): - self.should_succeed(dict(name=draft.name,rev=f'{r:02d}',ext=ext)) - self.should_404(dict(name=draft.name,rev='02')) - - with mock.patch('ietf.doc.models.DocumentInfo.pdfized', side_effect=URLFetchingError): + for r in range(0, 2): + self.should_succeed(dict(name=draft.name, rev=f"{r:02d}")) + for ext in ("pdf", "txt", "html", "anythingatall"): + self.should_succeed(dict(name=draft.name, rev=f"{r:02d}", ext=ext)) + self.should_404(dict(name=draft.name, rev="02")) + + with mock.patch( + "ietf.doc.models.DocumentInfo.pdfized", side_effect=URLFetchingError + ): url = urlreverse(self.view, kwargs=dict(name=rfc.name)) r = self.client.get(url) self.assertEqual(r.status_code, 200) self.assertContains(r, "Error while rendering PDF") + class NotifyValidationTests(TestCase): def test_notify_validation(self): valid_values = [ "foo@example.com, bar@example.com", "Foo Bar , baz@example.com", - "foo@example.com, ,bar@example.com,", # We're ignoring extra commas - "foo@example.com\nbar@example.com", # Yes, we're quietly accepting a newline as a comma + "foo@example.com, ,bar@example.com,", # We're ignoring extra commas + "foo@example.com\nbar@example.com", # Yes, we're quietly accepting a newline as a comma ] bad_nameaddr_values = [ "@example.com", @@ -3057,6 +4107,7 @@ def test_notify_validation(self): self.assertTrue("Invalid addresses" in f.errors["notify"][0]) self.assertTrue("Duplicate addresses" in f.errors["notify"][0]) + class CanRequestConflictReviewTests(TestCase): def test_gets_request_conflict_review_action_button(self): ise_draft = IndividualDraftFactory(stream_id="ise") @@ -3067,7 +4118,9 @@ def test_gets_request_conflict_review_action_button(self): target_string = "Begin IETF conflict review" - url = urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=irtf_draft.name)) + url = urlreverse( + "ietf.doc.views_doc.document_main", kwargs=dict(name=irtf_draft.name) + ) r = self.client.get(url) self.assertNotContains(r, target_string) self.client.login(username="secretary", password="secretary+password") @@ -3083,7 +4136,9 @@ def test_gets_request_conflict_review_action_button(self): self.assertNotContains(r, target_string) self.client.logout() - url = urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=ise_draft.name)) + url = urlreverse( + "ietf.doc.views_doc.document_main", kwargs=dict(name=ise_draft.name) + ) r = self.client.get(url) self.assertNotContains(r, target_string) self.client.login(username="secretary", password="secretary+password") @@ -3098,12 +4153,13 @@ def test_gets_request_conflict_review_action_button(self): r = self.client.get(url) self.assertContains(r, target_string) + class DocInfoMethodsTests(TestCase): def test_became_rfc(self): draft = WgDraftFactory() rfc = WgRfcFactory() - draft.relateddocument_set.create(relationship_id="became_rfc",target=rfc) + draft.relateddocument_set.create(relationship_id="became_rfc", target=rfc) self.assertEqual(draft.became_rfc(), rfc) self.assertEqual(rfc.came_from_draft(), draft) @@ -3112,17 +4168,26 @@ def test_became_rfc(self): self.assertIsNone(charter.came_from_draft()) def test_revisions(self): - draft = WgDraftFactory(rev="09",create_revisions=range(0,10)) - self.assertEqual(draft.revisions_by_dochistory(),[f"{i:02d}" for i in range(0,10)]) - self.assertEqual(draft.revisions_by_newrevisionevent(),[f"{i:02d}" for i in range(0,10)]) + draft = WgDraftFactory(rev="09", create_revisions=range(0, 10)) + self.assertEqual( + draft.revisions_by_dochistory(), [f"{i:02d}" for i in range(0, 10)] + ) + self.assertEqual( + draft.revisions_by_newrevisionevent(), [f"{i:02d}" for i in range(0, 10)] + ) rfc = WgRfcFactory() - self.assertEqual(rfc.revisions_by_newrevisionevent(),[]) - self.assertEqual(rfc.revisions_by_dochistory(),[]) + self.assertEqual(rfc.revisions_by_newrevisionevent(), []) + self.assertEqual(rfc.revisions_by_dochistory(), []) draft.history_set.filter(rev__lt="08").delete() draft.docevent_set.filter(newrevisiondocevent__rev="05").delete() - self.assertEqual(draft.revisions_by_dochistory(),[f"{i:02d}" for i in range(8,10)]) - self.assertEqual(draft.revisions_by_newrevisionevent(),[f"{i:02d}" for i in [*range(0,5), *range(6,10)]]) + self.assertEqual( + draft.revisions_by_dochistory(), [f"{i:02d}" for i in range(8, 10)] + ) + self.assertEqual( + draft.revisions_by_newrevisionevent(), + [f"{i:02d}" for i in [*range(0, 5), *range(6, 10)]], + ) def test_referenced_by_rfcs(self): # n.b., no significance to the ref* values in this test @@ -3140,28 +4205,42 @@ def test_referenced_by_rfcs_as_rfc_or_draft(self): draft = WgDraftFactory() rfc = WgRfcFactory() draft.relateddocument_set.create(relationship_id="became_rfc", target=rfc) - + # Draft referring to the rfc and the draft - should not be reported at all draft_referring_to_both = WgDraftFactory() - draft_referring_to_both.relateddocument_set.create(relationship_id="refnorm", target=draft) - draft_referring_to_both.relateddocument_set.create(relationship_id="refnorm", target=rfc) - + draft_referring_to_both.relateddocument_set.create( + relationship_id="refnorm", target=draft + ) + draft_referring_to_both.relateddocument_set.create( + relationship_id="refnorm", target=rfc + ) + # RFC referring only to the draft - should be reported for either the draft or the rfc rfc_referring_to_draft = WgRfcFactory() - rfc_referring_to_draft.relateddocument_set.create(relationship_id="refinfo", target=draft) + rfc_referring_to_draft.relateddocument_set.create( + relationship_id="refinfo", target=draft + ) # RFC referring only to the rfc - should be reported only for the rfc rfc_referring_to_rfc = WgRfcFactory() - rfc_referring_to_rfc.relateddocument_set.create(relationship_id="refinfo", target=rfc) + rfc_referring_to_rfc.relateddocument_set.create( + relationship_id="refinfo", target=rfc + ) # RFC referring only to the rfc - should be reported only for the rfc rfc_referring_to_rfc = WgRfcFactory() - rfc_referring_to_rfc.relateddocument_set.create(relationship_id="refinfo", target=rfc) + rfc_referring_to_rfc.relateddocument_set.create( + relationship_id="refinfo", target=rfc + ) # RFC referring to the rfc and the draft - should be reported for both rfc_referring_to_both = WgRfcFactory() - rfc_referring_to_both.relateddocument_set.create(relationship_id="refnorm", target=draft) - rfc_referring_to_both.relateddocument_set.create(relationship_id="refnorm", target=rfc) + rfc_referring_to_both.relateddocument_set.create( + relationship_id="refnorm", target=draft + ) + rfc_referring_to_both.relateddocument_set.create( + relationship_id="refnorm", target=rfc + ) self.assertCountEqual( draft.referenced_by_rfcs_as_rfc_or_draft(), @@ -3170,22 +4249,25 @@ def test_referenced_by_rfcs_as_rfc_or_draft(self): self.assertCountEqual( rfc.referenced_by_rfcs_as_rfc_or_draft(), - draft.targets_related.filter(source__type="rfc") | rfc.targets_related.filter(source__type="rfc"), + draft.targets_related.filter(source__type="rfc") + | rfc.targets_related.filter(source__type="rfc"), ) + class StateIndexTests(TestCase): def test_state_index(self): - url = urlreverse('ietf.doc.views_help.state_index') + url = urlreverse("ietf.doc.views_help.state_index") r = self.client.get(url) q = PyQuery(r.content) - content = [ e.text for e in q('#content table td a ') ] - names = StateType.objects.values_list('slug', flat=True) + content = [e.text for e in q("#content table td a ")] + names = StateType.objects.values_list("slug", flat=True) # The following doesn't cover all doc types, only a selection for name in names: - if not '-' in name: + if not "-" in name: self.assertIn(name, content) + class InvestigateTests(TestCase): settings_temp_path_overrides = TestCase.settings_temp_path_overrides + [ "AGENDA_PATH", @@ -3298,7 +4380,9 @@ def test_investigate_fragment_cache(self, mock_caches): self.assertTrue(mock_default_cache.set.called) expected_key = f"investigate_fragment:{sha384(b'this-is-active').hexdigest()}" self.assertEqual(mock_default_cache.set.call_args.kwargs["key"], expected_key) - cached_value = mock_default_cache.set.call_args.kwargs["value"] # hang on to this + cached_value = mock_default_cache.set.call_args.kwargs[ + "value" + ] # hang on to this mock_default_cache.reset_mock() # Check that a cached value is used @@ -3313,7 +4397,9 @@ def test_investigate_fragment_cache(self, mock_caches): list(result["can_verify"])[0].name, "draft-this-is-active-00.txt" ) # And that we used the cache - self.assertFalse(mock_path.called) # a proxy for "did the method do any real work" + self.assertFalse( + mock_path.called + ) # a proxy for "did the method do any real work" self.assertTrue(mock_default_cache.get.called) self.assertEqual(mock_default_cache.get.call_args, mock.call(expected_key)) @@ -3354,7 +4440,9 @@ def test_investigate_post(self, mock_investigate_fragment_task): login_testing_unauthorized(self, "secretary", url) # test some invalid cases - r = self.client.post(url, {"name_fragment": "short"}) # limit is >= 8 characters + r = self.client.post( + url, {"name_fragment": "short"} + ) # limit is >= 8 characters self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertEqual(len(q("#id_name_fragment.is-invalid")), 1) @@ -3365,13 +4453,16 @@ def test_investigate_post(self, mock_investigate_fragment_task): q = PyQuery(r.content) self.assertEqual(len(q("#id_name_fragment.is-invalid")), 1) self.assertFalse(mock_investigate_fragment_task.delay.called) - + # now a valid one mock_investigate_fragment_task.delay.return_value.id = "a-task-id" r = self.client.post(url, {"name_fragment": "this-is-a-valid-fragment"}) self.assertEqual(r.status_code, 200) self.assertTrue(mock_investigate_fragment_task.delay.called) - self.assertEqual(mock_investigate_fragment_task.delay.call_args, mock.call("this-is-a-valid-fragment")) + self.assertEqual( + mock_investigate_fragment_task.delay.call_args, + mock.call("this-is-a-valid-fragment"), + ) self.assertEqual(r.json(), {"id": "a-task-id"}) @mock.patch("ietf.doc.views_doc.AsyncResult") @@ -3383,7 +4474,9 @@ def test_investigate_post_task_id(self, mock_asyncresult): # First, test a non-successful result - this could be a failure or non-existent task id mock_result = mock_asyncresult.return_value mock_result.successful.return_value = False - r = self.client.post(url, {"name_fragment": "some-fragment", "task_id": "a-task-id"}) + r = self.client.post( + url, {"name_fragment": "some-fragment", "task_id": "a-task-id"} + ) self.assertContains(r, "The investigation task failed.", status_code=200) self.assertTrue(mock_asyncresult.called) self.assertEqual(mock_asyncresult.call_args, mock.call("a-task-id")) @@ -3402,15 +4495,21 @@ def test_investigate_post_task_id(self, mock_asyncresult): "can_verify": set(), "unverifiable_collections": set(), "unexpected": set(), - } + }, } - r = self.client.post(url, {"name_fragment": "some-fragment", "task_id": "a-task-id"}) + r = self.client.post( + url, {"name_fragment": "some-fragment", "task_id": "a-task-id"} + ) self.assertEqual(r.status_code, 200) self.assertTrue(mock_asyncresult.called) self.assertEqual(mock_asyncresult.call_args, mock.call("a-task-id")) mock_asyncresult.reset_mock() q = PyQuery(r.content) - self.assertEqual(q("#id_name_fragment").val(), "different-fragment", "name_fragment should be reset") + self.assertEqual( + q("#id_name_fragment").val(), + "different-fragment", + "name_fragment should be reset", + ) self.assertEqual(q("#id_task_id").val(), "", "task_id should be cleared") self.assertEqual(len(q("div#results")), 1) self.assertEqual(len(q("table#authenticated")), 0) @@ -3419,7 +4518,9 @@ def test_investigate_post_task_id(self, mock_asyncresult): # This file was created in setUp. It allows the view to render properly # but its location / content don't matter for this test otherwise. - a_file_that_exists = Path(settings.INTERNET_DRAFT_PATH) / "draft-this-is-active-00.txt" + a_file_that_exists = ( + Path(settings.INTERNET_DRAFT_PATH) / "draft-this-is-active-00.txt" + ) mock_result.get.return_value = { "name_fragment": "different-fragment", @@ -3427,15 +4528,21 @@ def test_investigate_post_task_id(self, mock_asyncresult): "can_verify": {a_file_that_exists}, "unverifiable_collections": {a_file_that_exists}, "unexpected": set(), - } + }, } - r = self.client.post(url, {"name_fragment": "some-fragment", "task_id": "a-task-id"}) + r = self.client.post( + url, {"name_fragment": "some-fragment", "task_id": "a-task-id"} + ) self.assertEqual(r.status_code, 200) self.assertTrue(mock_asyncresult.called) self.assertEqual(mock_asyncresult.call_args, mock.call("a-task-id")) mock_asyncresult.reset_mock() q = PyQuery(r.content) - self.assertEqual(q("#id_name_fragment").val(), "different-fragment", "name_fragment should be reset") + self.assertEqual( + q("#id_name_fragment").val(), + "different-fragment", + "name_fragment should be reset", + ) self.assertEqual(q("#id_task_id").val(), "", "task_id should be cleared") self.assertEqual(len(q("div#results")), 1) self.assertEqual(len(q("table#authenticated")), 1) @@ -3448,15 +4555,21 @@ def test_investigate_post_task_id(self, mock_asyncresult): "can_verify": set(), "unverifiable_collections": set(), "unexpected": {a_file_that_exists}, - } + }, } - r = self.client.post(url, {"name_fragment": "some-fragment", "task_id": "a-task-id"}) + r = self.client.post( + url, {"name_fragment": "some-fragment", "task_id": "a-task-id"} + ) self.assertEqual(r.status_code, 200) self.assertTrue(mock_asyncresult.called) self.assertEqual(mock_asyncresult.call_args, mock.call("a-task-id")) mock_asyncresult.reset_mock() q = PyQuery(r.content) - self.assertEqual(q("#id_name_fragment").val(), "different-fragment", "name_fragment should be reset") + self.assertEqual( + q("#id_name_fragment").val(), + "different-fragment", + "name_fragment should be reset", + ) self.assertEqual(q("#id_task_id").val(), "", "task_id should be cleared") self.assertEqual(len(q("div#results")), 1) self.assertEqual(len(q("table#authenticated")), 0) @@ -3473,7 +4586,9 @@ def test_doc_text_io_error(self): with mock.patch("ietf.doc.models.Path") as path_cls_mock: with mock.patch("ietf.doc.models.log.log") as log_mock: path_cls_mock.return_value.exists.return_value = True - path_cls_mock.return_value.open.return_value.__enter__.return_value.read.side_effect = IOError("Bad things happened") + path_cls_mock.return_value.open.return_value.__enter__.return_value.read.side_effect = IOError( + "Bad things happened" + ) text = d.text() self.assertIsNone(text) self.assertTrue(log_mock.called) diff --git a/ietf/doc/tests_ballot.py b/ietf/doc/tests_ballot.py index ec23f3d491..dfb7ae784f 100644 --- a/ietf/doc/tests_ballot.py +++ b/ietf/doc/tests_ballot.py @@ -7,17 +7,32 @@ from pyquery import PyQuery -import debug # pyflakes:ignore +import debug # pyflakes:ignore from django.test import RequestFactory from django.utils.text import slugify from django.urls import reverse as urlreverse from django.utils import timezone -from ietf.doc.models import (Document, State, DocEvent, - BallotPositionDocEvent, LastCallDocEvent, WriteupDocEvent, TelechatDocEvent) -from ietf.doc.factories import (DocumentFactory, IndividualDraftFactory, IndividualRfcFactory, WgDraftFactory, - BallotPositionDocEventFactory, BallotDocEventFactory, IRSGBallotDocEventFactory, RgDraftFactory) +from ietf.doc.models import ( + Document, + State, + DocEvent, + BallotPositionDocEvent, + LastCallDocEvent, + WriteupDocEvent, + TelechatDocEvent, +) +from ietf.doc.factories import ( + DocumentFactory, + IndividualDraftFactory, + IndividualRfcFactory, + WgDraftFactory, + BallotPositionDocEventFactory, + BallotDocEventFactory, + IRSGBallotDocEventFactory, + RgDraftFactory, +) from ietf.doc.templatetags.ietf_filters import can_defer from ietf.doc.utils import create_ballot_if_not_open from ietf.doc.views_ballot import parse_ballot_edit_return_point @@ -39,27 +54,34 @@ class EditPositionTests(TestCase): def test_edit_position(self): ad = Person.objects.get(user__username="ad") - draft = IndividualDraftFactory(ad=ad,stream_id='ietf') - ballot = create_ballot_if_not_open(None, draft, ad, 'approve') - url = urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=draft.name, - ballot_id=ballot.pk)) + draft = IndividualDraftFactory(ad=ad, stream_id="ietf") + ballot = create_ballot_if_not_open(None, draft, ad, "approve") + url = urlreverse( + "ietf.doc.views_ballot.edit_position", + kwargs=dict(name=draft.name, ballot_id=ballot.pk), + ) login_testing_unauthorized(self, "ad", url) ad = Person.objects.get(name="Areað Irector") - + # normal get r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertTrue(len(q('form input[name=position]')) > 0) - self.assertEqual(len(q('form textarea[name=comment]')), 1) + self.assertTrue(len(q("form input[name=position]")) > 0) + self.assertEqual(len(q("form textarea[name=comment]")), 1) # vote events_before = draft.docevent_set.count() - - r = self.client.post(url, dict(position="discuss", - discuss=" This is a discussion test. \n ", - comment=" This is a test. \n ")) + + r = self.client.post( + url, + dict( + position="discuss", + discuss=" This is a discussion test. \n ", + comment=" This is a test. \n ", + ), + ) self.assertEqual(r.status_code, 302) pos = draft.latest_event(BallotPositionDocEvent, balloter=ad) @@ -81,7 +103,7 @@ def test_edit_position(self): self.assertEqual(pos.pos.slug, "noobj") self.assertEqual(draft.docevent_set.count(), events_before + 1) self.assertTrue("Position for" in pos.desc) - + # clear vote events_before = draft.docevent_set.count() r = self.client.post(url, dict(position="norecord")) @@ -103,12 +125,12 @@ def test_edit_position(self): self.assertEqual(pos.pos.slug, "norecord") self.assertEqual(draft.docevent_set.count(), events_before + 2) self.assertTrue("Ballot comment text updated" in pos.desc) - + def test_api_set_position(self): ad = Person.objects.get(name="Areað Irector") draft = WgDraftFactory(ad=ad) - url = urlreverse('ietf.doc.views_ballot.api_set_position') - create_ballot_if_not_open(None, draft, ad, 'approve') + url = urlreverse("ietf.doc.views_ballot.api_set_position") + create_ballot_if_not_open(None, draft, ad, "approve") ad.user.last_login = timezone.now() ad.user.save() apikey = PersonalApiKeyFactory(endpoint=url, person=ad) @@ -117,13 +139,16 @@ def test_api_set_position(self): events_before = draft.docevent_set.count() mailbox_before = len(outbox) - r = self.client.post(url, dict( - apikey=apikey.hash(), - doc=draft.name, - position="discuss", - discuss=" This is a discussion test. \n ", - comment=" This is a test. \n ") - ) + r = self.client.post( + url, + dict( + apikey=apikey.hash(), + doc=draft.name, + position="discuss", + discuss=" This is a discussion test. \n ", + comment=" This is a test. \n ", + ), + ) self.assertContains(r, "Done") pos = draft.latest_event(BallotPositionDocEvent, balloter=ad) @@ -139,7 +164,9 @@ def test_api_set_position(self): # recast vote events_before = draft.docevent_set.count() mailbox_before = len(outbox) - r = self.client.post(url, dict(apikey=apikey.hash(), doc=draft.name, position="noobj")) + r = self.client.post( + url, dict(apikey=apikey.hash(), doc=draft.name, position="noobj") + ) self.assertEqual(r.status_code, 200) draft = Document.objects.get(name=draft.name) @@ -149,15 +176,17 @@ def test_api_set_position(self): self.assertTrue("Position for" in pos.desc) self.assertEqual(len(outbox), mailbox_before + 1) m = outbox[-1] - self.assertIn('No Objection', m['Subject']) - self.assertIn('iesg@', m['To']) - self.assertIn(draft.name, m['Cc']) - self.assertIn(draft.group.acronym+'-chairs@', m['Cc']) + self.assertIn("No Objection", m["Subject"]) + self.assertIn("iesg@", m["To"]) + self.assertIn(draft.name, m["Cc"]) + self.assertIn(draft.group.acronym + "-chairs@", m["Cc"]) # clear vote events_before = draft.docevent_set.count() mailbox_before = len(outbox) - r = self.client.post(url, dict(apikey=apikey.hash(), doc=draft.name, position="norecord")) + r = self.client.post( + url, dict(apikey=apikey.hash(), doc=draft.name, position="norecord") + ) self.assertEqual(r.status_code, 200) draft = Document.objects.get(name=draft.name) @@ -167,12 +196,20 @@ def test_api_set_position(self): self.assertTrue("Position for" in pos.desc) self.assertEqual(len(outbox), mailbox_before + 1) m = outbox[-1] - self.assertIn('No Record', m['Subject']) + self.assertIn("No Record", m["Subject"]) # change comment events_before = draft.docevent_set.count() mailbox_before = len(outbox) - r = self.client.post(url, dict(apikey=apikey.hash(), doc=draft.name, position="norecord", comment="New comment.")) + r = self.client.post( + url, + dict( + apikey=apikey.hash(), + doc=draft.name, + position="norecord", + comment="New comment.", + ), + ) self.assertEqual(r.status_code, 200) draft = Document.objects.get(name=draft.name) @@ -182,15 +219,17 @@ def test_api_set_position(self): self.assertTrue("Ballot comment text updated" in pos.desc) self.assertEqual(len(outbox), mailbox_before + 1) m = outbox[-1] - self.assertIn('COMMENT', m['Subject']) - self.assertIn('New comment', get_payload_text(m)) - + self.assertIn("COMMENT", m["Subject"]) + self.assertIn("New comment", get_payload_text(m)) def test_edit_position_as_secretary(self): draft = IndividualDraftFactory() ad = Person.objects.get(user__username="ad") - ballot = create_ballot_if_not_open(None, draft, ad, 'approve') - url = urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=draft.name, ballot_id=ballot.pk)) + ballot = create_ballot_if_not_open(None, draft, ad, "approve") + url = urlreverse( + "ietf.doc.views_ballot.edit_position", + kwargs=dict(name=draft.name, ballot_id=ballot.pk), + ) ad = Person.objects.get(name="Areað Irector") url += "?balloter=%s" % ad.pk login_testing_unauthorized(self, "secretary", url) @@ -199,7 +238,7 @@ def test_edit_position_as_secretary(self): r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertTrue(len(q('form input[name=position]')) > 0) + self.assertTrue(len(q("form input[name=position]")) > 0) # vote on behalf of AD # events_before = draft.docevent_set.count() @@ -215,9 +254,12 @@ def test_edit_position_as_secretary(self): def test_cannot_edit_position_as_pre_ad(self): draft = IndividualDraftFactory() ad = Person.objects.get(user__username="ad") - ballot = create_ballot_if_not_open(None, draft, ad, 'approve') - url = urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=draft.name, ballot_id=ballot.pk)) - + ballot = create_ballot_if_not_open(None, draft, ad, "approve") + url = urlreverse( + "ietf.doc.views_ballot.edit_position", + kwargs=dict(name=draft.name, ballot_id=ballot.pk), + ) + # transform to pre-ad ad_role = Role.objects.filter(name="ad")[0] ad_role.name_id = "pre-ad" @@ -229,28 +271,46 @@ def test_cannot_edit_position_as_pre_ad(self): # but not touch r = self.client.post(url, dict(position="discuss", discuss="Test discuss text")) self.assertEqual(r.status_code, 403) - + # N.B. This test needs to be rewritten to exercise all types of ballots (iesg, irsg, rsab) # and test against the output of the mailtriggers instead of looking for hardcoded values # in the To and CC results. See #7864 def test_send_ballot_comment(self): ad = Person.objects.get(user__username="ad") - draft = WgDraftFactory(ad=ad,group__acronym='mars') + draft = WgDraftFactory(ad=ad, group__acronym="mars") draft.notify = "somebody@example.com" - draft.save_with_history([DocEvent.objects.create(doc=draft, rev=draft.rev, type="changed_document", by=Person.objects.get(user__username="secretary"), desc="Test")]) + draft.save_with_history( + [ + DocEvent.objects.create( + doc=draft, + rev=draft.rev, + type="changed_document", + by=Person.objects.get(user__username="secretary"), + desc="Test", + ) + ] + ) - ballot = create_ballot_if_not_open(None, draft, ad, 'approve') + ballot = create_ballot_if_not_open(None, draft, ad, "approve") BallotPositionDocEvent.objects.create( - doc=draft, rev=draft.rev, type="changed_ballot_position", - by=ad, balloter=ad, ballot=ballot, pos=BallotPositionName.objects.get(slug="discuss"), + doc=draft, + rev=draft.rev, + type="changed_ballot_position", + by=ad, + balloter=ad, + ballot=ballot, + pos=BallotPositionName.objects.get(slug="discuss"), discuss="This draft seems to be lacking a clearer title?", discuss_time=timezone.now(), comment="Test!", - comment_time=timezone.now()) - - url = urlreverse('ietf.doc.views_ballot.send_ballot_comment', kwargs=dict(name=draft.name, - ballot_id=ballot.pk)) + comment_time=timezone.now(), + ) + + url = urlreverse( + "ietf.doc.views_ballot.send_ballot_comment", + kwargs=dict(name=draft.name, ballot_id=ballot.pk), + ) login_testing_unauthorized(self, "ad", url) # normal get @@ -262,79 +322,100 @@ def test_send_ballot_comment(self): # send mailbox_before = len(outbox) - r = self.client.post(url, dict(extra_cc="test298347@example.com", cc_choices=['doc_notify','doc_group_chairs'])) + r = self.client.post( + url, + dict( + extra_cc="test298347@example.com", + cc_choices=["doc_notify", "doc_group_chairs"], + ), + ) self.assertEqual(r.status_code, 302) self.assertEqual(len(outbox), mailbox_before + 1) m = outbox[-1] - self.assertTrue("COMMENT" in m['Subject']) - self.assertTrue("DISCUSS" in m['Subject']) - self.assertTrue(draft.name in m['Subject']) + self.assertTrue("COMMENT" in m["Subject"]) + self.assertTrue("DISCUSS" in m["Subject"]) + self.assertTrue(draft.name in m["Subject"]) self.assertTrue("clearer title" in str(m)) self.assertTrue("Test!" in str(m)) - self.assertTrue("iesg@" in m['To']) + self.assertTrue("iesg@" in m["To"]) # cc_choice doc_group_chairs - self.assertTrue("mars-chairs@" in m['Cc']) + self.assertTrue("mars-chairs@" in m["Cc"]) # cc_choice doc_notify - self.assertTrue("somebody@example.com" in m['Cc']) + self.assertTrue("somebody@example.com" in m["Cc"]) # cc_choice doc_group_email_list was not selected - self.assertFalse(draft.group.list_email in m['Cc']) - # extra-cc - self.assertTrue("test298347@example.com" in m['Cc']) + self.assertFalse(draft.group.list_email in m["Cc"]) + # extra-cc + self.assertTrue("test298347@example.com" in m["Cc"]) r = self.client.post(url, dict(cc="")) self.assertEqual(r.status_code, 302) self.assertEqual(len(outbox), mailbox_before + 2) m = outbox[-1] - self.assertTrue("iesg@" in m['To']) - self.assertFalse(m['Cc'] and draft.group.list_email in m['Cc']) + self.assertTrue("iesg@" in m["To"]) + self.assertFalse(m["Cc"] and draft.group.list_email in m["Cc"]) class BallotWriteupsTests(TestCase): def test_edit_last_call_text(self): - draft = IndividualDraftFactory(ad=Person.objects.get(user__username='ad'),states=[('draft','active'),('draft-iesg','ad-eval')]) - url = urlreverse('ietf.doc.views_ballot.lastcalltext', kwargs=dict(name=draft.name)) + draft = IndividualDraftFactory( + ad=Person.objects.get(user__username="ad"), + states=[("draft", "active"), ("draft-iesg", "ad-eval")], + ) + url = urlreverse( + "ietf.doc.views_ballot.lastcalltext", kwargs=dict(name=draft.name) + ) login_testing_unauthorized(self, "secretary", url) # normal get r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertEqual(len(q('textarea[name=last_call_text]')), 1) + self.assertEqual(len(q("textarea[name=last_call_text]")), 1) self.assertTrue(q('[type=submit]:contains("Save")')) # we're Secretariat, so we got The Link self.assertEqual(len(q('a:contains("Issue last call")')), 1) - + # subject error - r = self.client.post(url, dict( - last_call_text="Subject: test\r\nhello\r\n\r\n", - save_last_call_text="1")) + r = self.client.post( + url, + dict( + last_call_text="Subject: test\r\nhello\r\n\r\n", save_last_call_text="1" + ), + ) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertTrue(len(q('form .is-invalid')) > 0) + self.assertTrue(len(q("form .is-invalid")) > 0) # save - r = self.client.post(url, dict( - last_call_text="This is a simple test.", - save_last_call_text="1")) + r = self.client.post( + url, dict(last_call_text="This is a simple test.", save_last_call_text="1") + ) self.assertEqual(r.status_code, 200) draft = Document.objects.get(name=draft.name) - self.assertTrue("This is a simple test" in draft.latest_event(WriteupDocEvent, type="changed_last_call_text").text) + self.assertTrue( + "This is a simple test" + in draft.latest_event(WriteupDocEvent, type="changed_last_call_text").text + ) # test regenerate - r = self.client.post(url, dict( - last_call_text="This is a simple test.", - regenerate_last_call_text="1")) + r = self.client.post( + url, + dict( + last_call_text="This is a simple test.", regenerate_last_call_text="1" + ), + ) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) text = q("[name=last_call_text]").text() self.assertTrue("Subject: Last Call" in text) - def test_request_last_call(self): ad = Person.objects.get(user__username="ad") - draft = IndividualDraftFactory(ad=ad,states=[('draft-iesg','iesg-eva')]) - url = urlreverse('ietf.doc.views_ballot.lastcalltext', kwargs=dict(name=draft.name)) + draft = IndividualDraftFactory(ad=ad, states=[("draft-iesg", "iesg-eva")]) + url = urlreverse( + "ietf.doc.views_ballot.lastcalltext", kwargs=dict(name=draft.name) + ) login_testing_unauthorized(self, "secretary", url) # give us an announcement to send @@ -346,54 +427,67 @@ def test_request_last_call(self): mailbox_before = len(outbox) # send - r = self.client.post(url, dict( - last_call_text=text, - send_last_call_request="1")) + r = self.client.post(url, dict(last_call_text=text, send_last_call_request="1")) draft = Document.objects.get(name=draft.name) self.assertEqual(draft.get_state_slug("draft-iesg"), "lc-req") self.assertCountEqual(draft.action_holders.all(), [ad]) - self.assertIn('Changed action holders', draft.latest_event(type='changed_action_holders').desc) + self.assertIn( + "Changed action holders", + draft.latest_event(type="changed_action_holders").desc, + ) self.assertEqual(len(outbox), mailbox_before + 1) - self.assertTrue("Last Call" in outbox[-1]['Subject']) - self.assertTrue(draft.name in outbox[-1]['Subject']) - self.assertTrue('iesg-secretary@' in outbox[-1]['To']) - self.assertTrue('aread@' in outbox[-1]['Cc']) + self.assertTrue("Last Call" in outbox[-1]["Subject"]) + self.assertTrue(draft.name in outbox[-1]["Subject"]) + self.assertTrue("iesg-secretary@" in outbox[-1]["To"]) + self.assertTrue("aread@" in outbox[-1]["Cc"]) def test_edit_ballot_writeup(self): - draft = IndividualDraftFactory(states=[('draft','active'),('draft-iesg','iesg-eva')], stream_id='ietf') - url = urlreverse('ietf.doc.views_ballot.ballot_writeupnotes', kwargs=dict(name=draft.name)) + draft = IndividualDraftFactory( + states=[("draft", "active"), ("draft-iesg", "iesg-eva")], stream_id="ietf" + ) + url = urlreverse( + "ietf.doc.views_ballot.ballot_writeupnotes", kwargs=dict(name=draft.name) + ) login_testing_unauthorized(self, "secretary", url) # add a IANA review note - draft.set_state(State.objects.get(used=True, type="draft-iana-review", slug="not-ok")) - DocEvent.objects.create(type="iana_review", - doc=draft, - rev=draft.rev, - by=Person.objects.get(user__username="iana"), - desc="IANA does not approve of this document, it does not make sense.", - ) + draft.set_state( + State.objects.get(used=True, type="draft-iana-review", slug="not-ok") + ) + DocEvent.objects.create( + type="iana_review", + doc=draft, + rev=draft.rev, + by=Person.objects.get(user__username="iana"), + desc="IANA does not approve of this document, it does not make sense.", + ) # normal get r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertEqual(len(q('textarea[name=ballot_writeup]')), 1) + self.assertEqual(len(q("textarea[name=ballot_writeup]")), 1) self.assertTrue(q('[type=submit]:contains("Save")')) self.assertContains(r, "IANA does not") # save - r = self.client.post(url, dict( - ballot_writeup="This is a simple test.", - save_ballot_writeup="1")) + r = self.client.post( + url, dict(ballot_writeup="This is a simple test.", save_ballot_writeup="1") + ) self.assertEqual(r.status_code, 200) d = Document.objects.get(name=draft.name) - self.assertTrue("This is a simple test" in d.latest_event(WriteupDocEvent, type="changed_ballot_writeup_text").text) - self.assertTrue('iesg-eva' == d.get_state_slug('draft-iesg')) + self.assertTrue( + "This is a simple test" + in d.latest_event(WriteupDocEvent, type="changed_ballot_writeup_text").text + ) + self.assertTrue("iesg-eva" == d.get_state_slug("draft-iesg")) def test_edit_ballot_writeup_unauthorized_stream(self): # Test that accessing a document from unauthorized (irtf) stream returns a 404 error draft = RgDraftFactory() - url = urlreverse('ietf.doc.views_ballot.ballot_writeupnotes', kwargs=dict(name=draft.name)) + url = urlreverse( + "ietf.doc.views_ballot.ballot_writeupnotes", kwargs=dict(name=draft.name) + ) login_testing_unauthorized(self, "ad", url) r = self.client.get(url) @@ -401,35 +495,44 @@ def test_edit_ballot_writeup_unauthorized_stream(self): def test_edit_ballot_writeup_invalid_name(self): # Test that accessing a non-existent document returns a 404 error - url = urlreverse('ietf.doc.views_ballot.ballot_writeupnotes', kwargs=dict(name="invalid_name")) + url = urlreverse( + "ietf.doc.views_ballot.ballot_writeupnotes", + kwargs=dict(name="invalid_name"), + ) login_testing_unauthorized(self, "ad", url) r = self.client.get(url) self.assertEqual(r.status_code, 404) def test_edit_ballot_writeup_already_approved(self): - draft = IndividualDraftFactory(states=[('draft','active'),('draft-iesg','approved')], stream_id='ietf') - url = urlreverse('ietf.doc.views_ballot.ballot_writeupnotes', kwargs=dict(name=draft.name)) + draft = IndividualDraftFactory( + states=[("draft", "active"), ("draft-iesg", "approved")], stream_id="ietf" + ) + url = urlreverse( + "ietf.doc.views_ballot.ballot_writeupnotes", kwargs=dict(name=draft.name) + ) login_testing_unauthorized(self, "secretary", url) # normal get r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertEqual(len(q('textarea[name=ballot_writeup]')), 1) + self.assertEqual(len(q("textarea[name=ballot_writeup]")), 1) self.assertTrue(q('[type=submit]:contains("Save")')) # save - r = self.client.post(url, dict( - ballot_writeup="This is a simple test.", - save_ballot_writeup="1")) + r = self.client.post( + url, dict(ballot_writeup="This is a simple test.", save_ballot_writeup="1") + ) self.assertEqual(r.status_code, 200) d = Document.objects.get(name=draft.name) - self.assertTrue('approved' == d.get_state_slug('draft-iesg')) + self.assertTrue("approved" == d.get_state_slug("draft-iesg")) def test_edit_ballot_rfceditornote(self): draft = IndividualDraftFactory() - url = urlreverse('ietf.doc.views_ballot.ballot_rfceditornote', kwargs=dict(name=draft.name)) + url = urlreverse( + "ietf.doc.views_ballot.ballot_rfceditornote", kwargs=dict(name=draft.name) + ) login_testing_unauthorized(self, "secretary", url) # add a note to the RFC Editor @@ -439,114 +542,139 @@ def test_edit_ballot_rfceditornote(self): desc="Changed text", type="changed_rfc_editor_note_text", text="This is a note for the RFC Editor.", - by=Person.objects.get(name="(System)")) + by=Person.objects.get(name="(System)"), + ) # normal get r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertEqual(len(q('textarea[name=rfc_editor_note]')), 1) + self.assertEqual(len(q("textarea[name=rfc_editor_note]")), 1) self.assertTrue(q('[type=submit]:contains("Save")')) - self.assertContains(r, "") + self.assertContains(r, '') self.assertContains(r, "This is a note for the RFC Editor") # save with a note empty_outbox() - r = self.client.post(url, dict( - rfc_editor_note="This is a simple test.", - save_ballot_rfceditornote="1")) + r = self.client.post( + url, + dict( + rfc_editor_note="This is a simple test.", save_ballot_rfceditornote="1" + ), + ) self.assertEqual(r.status_code, 302) draft = Document.objects.get(name=draft.name) self.assertTrue(draft.has_rfc_editor_note()) - self.assertTrue("This is a simple test" in draft.latest_event(WriteupDocEvent, type="changed_rfc_editor_note_text").text) + self.assertTrue( + "This is a simple test" + in draft.latest_event( + WriteupDocEvent, type="changed_rfc_editor_note_text" + ).text + ) self.assertEqual(len(outbox), 0) # clear the existing note - r = self.client.post(url, dict( - rfc_editor_note=" ", - clear_ballot_rfceditornote="1")) + r = self.client.post( + url, dict(rfc_editor_note=" ", clear_ballot_rfceditornote="1") + ) self.assertEqual(r.status_code, 200) draft = Document.objects.get(name=draft.name) self.assertFalse(draft.has_rfc_editor_note()) # Add a note after the doc is approved empty_outbox() - draft.set_state(State.objects.get(type='draft-iesg',slug='approved')) - r = self.client.post(url, dict( - rfc_editor_note='This is a new note.', - save_ballot_rfceditornote="1")) + draft.set_state(State.objects.get(type="draft-iesg", slug="approved")) + r = self.client.post( + url, + dict(rfc_editor_note="This is a new note.", save_ballot_rfceditornote="1"), + ) self.assertEqual(r.status_code, 302) - self.assertEqual(len(outbox),1) - self.assertIn('RFC Editor note changed',outbox[-1]['Subject']) + self.assertEqual(len(outbox), 1) + self.assertIn("RFC Editor note changed", outbox[-1]["Subject"]) def test_issue_ballot(self): ad = Person.objects.get(user__username="ad") - for case in ('none','past','future'): - draft = IndividualDraftFactory(ad=ad, stream_id='ietf') - if case in ('past','future'): + for case in ("none", "past", "future"): + draft = IndividualDraftFactory(ad=ad, stream_id="ietf") + if case in ("past", "future"): LastCallDocEvent.objects.create( - by=Person.objects.get(name='(System)'), - type='sent_last_call', + by=Person.objects.get(name="(System)"), + type="sent_last_call", doc=draft, rev=draft.rev, - desc='issued last call', - expires = timezone.now()+datetime.timedelta(days = 1 if case=='future' else -1) + desc="issued last call", + expires=timezone.now() + + datetime.timedelta(days=1 if case == "future" else -1), ) - url = urlreverse('ietf.doc.views_ballot.ballot_writeupnotes', kwargs=dict(name=draft.name)) + url = urlreverse( + "ietf.doc.views_ballot.ballot_writeupnotes", + kwargs=dict(name=draft.name), + ) login_testing_unauthorized(self, "ad", url) - empty_outbox() - - r = self.client.post(url, dict( - ballot_writeup="This is a test.", - issue_ballot="1")) + + r = self.client.post( + url, dict(ballot_writeup="This is a test.", issue_ballot="1") + ) self.assertEqual(r.status_code, 200) draft = Document.objects.get(name=draft.name) self.assertTrue(draft.latest_event(type="sent_ballot_announcement")) self.assertEqual(len(outbox), 2) - self.assertTrue('Ballot issued:' in outbox[-2]['Subject']) - self.assertTrue('iesg@' in outbox[-2]['To']) - self.assertTrue('Ballot issued:' in outbox[-1]['Subject']) - self.assertTrue('drafts-eval@' in outbox[-1]['To']) - self.assertTrue('X-IETF-Draft-string' in outbox[-1]) - if case=='none': - self.assertNotIn('call expire', get_payload_text(outbox[-1])) - elif case=='past': - self.assertIn('call expired', get_payload_text(outbox[-1])) + self.assertTrue("Ballot issued:" in outbox[-2]["Subject"]) + self.assertTrue("iesg@" in outbox[-2]["To"]) + self.assertTrue("Ballot issued:" in outbox[-1]["Subject"]) + self.assertTrue("drafts-eval@" in outbox[-1]["To"]) + self.assertTrue("X-IETF-Draft-string" in outbox[-1]) + if case == "none": + self.assertNotIn("call expire", get_payload_text(outbox[-1])) + elif case == "past": + self.assertIn("call expired", get_payload_text(outbox[-1])) else: - self.assertIn('call expires', get_payload_text(outbox[-1])) + self.assertIn("call expires", get_payload_text(outbox[-1])) self.client.logout() def test_issue_ballot_auto_state_change(self): ad = Person.objects.get(user__username="ad") - draft = IndividualDraftFactory(ad=ad, states=[('draft','active'),('draft-iesg','writeupw')], stream_id='ietf') - url = urlreverse('ietf.doc.views_ballot.ballot_writeupnotes', kwargs=dict(name=draft.name)) + draft = IndividualDraftFactory( + ad=ad, + states=[("draft", "active"), ("draft-iesg", "writeupw")], + stream_id="ietf", + ) + url = urlreverse( + "ietf.doc.views_ballot.ballot_writeupnotes", kwargs=dict(name=draft.name) + ) login_testing_unauthorized(self, "secretary", url) # normal get r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertEqual(len(q('textarea[name=ballot_writeup]')), 1) - self.assertFalse(q('[class=form-text]:contains("not completed IETF Last Call")')) + self.assertEqual(len(q("textarea[name=ballot_writeup]")), 1) + self.assertFalse( + q('[class=form-text]:contains("not completed IETF Last Call")') + ) self.assertTrue(q('[type=submit]:contains("Save")')) self.assertCountEqual(draft.action_holders.all(), []) # save - r = self.client.post(url, dict( - ballot_writeup="This is a simple test.", - issue_ballot="1")) + r = self.client.post( + url, dict(ballot_writeup="This is a simple test.", issue_ballot="1") + ) self.assertEqual(r.status_code, 200) d = Document.objects.get(name=draft.name) - self.assertTrue('iesg-eva' == d.get_state_slug('draft-iesg')) + self.assertTrue("iesg-eva" == d.get_state_slug("draft-iesg")) self.assertCountEqual(draft.action_holders.all(), [ad]) def test_issue_ballot_warn_if_early(self): ad = Person.objects.get(user__username="ad") - draft = IndividualDraftFactory(ad=ad, states=[('draft','active'),('draft-iesg','lc')], stream_id='ietf') - url = urlreverse('ietf.doc.views_ballot.ballot_writeupnotes', kwargs=dict(name=draft.name)) + draft = IndividualDraftFactory( + ad=ad, states=[("draft", "active"), ("draft-iesg", "lc")], stream_id="ietf" + ) + url = urlreverse( + "ietf.doc.views_ballot.ballot_writeupnotes", kwargs=dict(name=draft.name) + ) login_testing_unauthorized(self, "secretary", url) # expect warning about issuing a ballot before IETF Last Call is done @@ -554,111 +682,175 @@ def test_issue_ballot_warn_if_early(self): r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertEqual(len(q('textarea[name=ballot_writeup]')), 1) - self.assertTrue(q('[class=text-danger]:contains("not completed IETF Last Call")')) + self.assertEqual(len(q("textarea[name=ballot_writeup]")), 1) + self.assertTrue( + q('[class=text-danger]:contains("not completed IETF Last Call")') + ) self.assertTrue(q('[type=submit]:contains("Save")')) # Last call exists but hasn't expired LastCallDocEvent.objects.create( doc=draft, - expires=datetime_today()+datetime.timedelta(days=14), - by=Person.objects.get(name="(System)") + expires=datetime_today() + datetime.timedelta(days=14), + by=Person.objects.get(name="(System)"), ) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertTrue(q('[class=text-danger]:contains("not completed IETF Last Call")')) + self.assertTrue( + q('[class=text-danger]:contains("not completed IETF Last Call")') + ) # Last call exists and has expired - LastCallDocEvent.objects.filter(doc=draft).update(expires=datetime_today()-datetime.timedelta(days=2)) + LastCallDocEvent.objects.filter(doc=draft).update( + expires=datetime_today() - datetime.timedelta(days=2) + ) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertFalse(q('[class=text-danger]:contains("not completed IETF Last Call")')) + self.assertFalse( + q('[class=text-danger]:contains("not completed IETF Last Call")') + ) for state_slug in ["lc", "ad-eval"]: - draft.set_state(State.objects.get(type="draft-iesg",slug=state_slug)) + draft.set_state(State.objects.get(type="draft-iesg", slug=state_slug)) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertTrue(q('[class=text-danger]:contains("It would be unexpected to issue a ballot while in this state.")')) + self.assertTrue( + q( + '[class=text-danger]:contains("It would be unexpected to issue a ballot while in this state.")' + ) + ) - draft.set_state(State.objects.get(type="draft-iesg",slug="writeupw")) + draft.set_state(State.objects.get(type="draft-iesg", slug="writeupw")) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertFalse(q('[class=text-danger]:contains("It would be unexpected to issue a ballot while in this state.")')) - + self.assertFalse( + q( + '[class=text-danger]:contains("It would be unexpected to issue a ballot while in this state.")' + ) + ) def test_edit_approval_text(self): ad = Person.objects.get(user__username="ad") - draft = WgDraftFactory(ad=ad,states=[('draft','active'),('draft-iesg','iesg-eva')],intended_std_level_id='ps',group__parent=Group.objects.get(acronym='farfut')) - url = urlreverse('ietf.doc.views_ballot.ballot_approvaltext', kwargs=dict(name=draft.name)) + draft = WgDraftFactory( + ad=ad, + states=[("draft", "active"), ("draft-iesg", "iesg-eva")], + intended_std_level_id="ps", + group__parent=Group.objects.get(acronym="farfut"), + ) + url = urlreverse( + "ietf.doc.views_ballot.ballot_approvaltext", kwargs=dict(name=draft.name) + ) login_testing_unauthorized(self, "secretary", url) # normal get r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertEqual(len(q('textarea[name=approval_text]')), 1) + self.assertEqual(len(q("textarea[name=approval_text]")), 1) self.assertTrue(q('[type=submit]:contains("Save")')) # save - r = self.client.post(url, dict( - approval_text="This is a simple test.", - save_approval_text="1")) + r = self.client.post( + url, dict(approval_text="This is a simple test.", save_approval_text="1") + ) self.assertEqual(r.status_code, 200) draft = Document.objects.get(name=draft.name) - self.assertTrue("This is a simple test" in draft.latest_event(WriteupDocEvent, type="changed_ballot_approval_text").text) + self.assertTrue( + "This is a simple test" + in draft.latest_event( + WriteupDocEvent, type="changed_ballot_approval_text" + ).text + ) # test regenerate r = self.client.post(url, dict(regenerate_approval_text="1")) self.assertEqual(r.status_code, 200) - draft = Document.objects.get(name=draft.name) - self.assertTrue("Subject: Protocol Action" in draft.latest_event(WriteupDocEvent, type="changed_ballot_approval_text").text) + draft = Document.objects.get(name=draft.name) + self.assertTrue( + "Subject: Protocol Action" + in draft.latest_event( + WriteupDocEvent, type="changed_ballot_approval_text" + ).text + ) # test regenerate when it's a disapprove - draft.set_state(State.objects.get(used=True, type="draft-iesg", slug="nopubadw")) + draft.set_state( + State.objects.get(used=True, type="draft-iesg", slug="nopubadw") + ) r = self.client.post(url, dict(regenerate_approval_text="1")) self.assertEqual(r.status_code, 200) draft = Document.objects.get(name=draft.name) - self.assertIn("NOT be published", unwrap(draft.latest_event(WriteupDocEvent, type="changed_ballot_approval_text").text)) + self.assertIn( + "NOT be published", + unwrap( + draft.latest_event( + WriteupDocEvent, type="changed_ballot_approval_text" + ).text + ), + ) # test regenerate when it's a conflict review draft.group = Group.objects.get(type="individ") draft.stream_id = "irtf" - draft.set_state(State.objects.get(used=True, type="draft-iesg", slug="iesg-eva")) - draft.save_with_history([DocEvent.objects.create(doc=draft, rev=draft.rev, type="changed_document", by=Person.objects.get(user__username="secretary"), desc="Test")]) + draft.set_state( + State.objects.get(used=True, type="draft-iesg", slug="iesg-eva") + ) + draft.save_with_history( + [ + DocEvent.objects.create( + doc=draft, + rev=draft.rev, + type="changed_document", + by=Person.objects.get(user__username="secretary"), + desc="Test", + ) + ] + ) r = self.client.post(url, dict(regenerate_approval_text="1")) self.assertEqual(r.status_code, 200) - self.assertTrue("Subject: Results of IETF-conflict review" in draft.latest_event(WriteupDocEvent, type="changed_ballot_approval_text").text) - + self.assertTrue( + "Subject: Results of IETF-conflict review" + in draft.latest_event( + WriteupDocEvent, type="changed_ballot_approval_text" + ).text + ) + def test_edit_verify_permissions(self): def verify_fail(username, url): if username: - self.client.login(username=username, password=username+"+password") + self.client.login(username=username, password=username + "+password") r = self.client.get(url) - self.assertEqual(r.status_code,403) + self.assertEqual(r.status_code, 403) def verify_can_see(username, url): - self.client.login(username=username, password=username+"+password") + self.client.login(username=username, password=username + "+password") r = self.client.get(url) - self.assertEqual(r.status_code,200) + self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertEqual(len(q("