From d74d5ed233d0b886a547fbd76bcc2767e932f177 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 13 Dec 2023 11:54:22 -0600 Subject: [PATCH 1/8] fix: ignore attempts to look for versions of rfcs, but find the rfc. --- ietf/doc/tests_utils.py | 6 ++++++ ietf/doc/utils.py | 7 ++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/ietf/doc/tests_utils.py b/ietf/doc/tests_utils.py index 59223b1f8dc..248ac345afa 100644 --- a/ietf/doc/tests_utils.py +++ b/ietf/doc/tests_utils.py @@ -309,6 +309,12 @@ def do_fuzzy_find_documents_rfc_test(self, name): found = fuzzy_find_documents(draft.name, '22') self.assertCountEqual(found.documents, [draft], 'Should find document even if rev does not exist') + + # by rfc name mistakenly trying to provide a revision + found = fuzzy_find_documents(rfc.name+"-22") + self.assertCountEqual(found.documents, [rfc], "Should ignore versions when fuzzyfinding RFCs" ) + found = fuzzy_find_documents(rfc.name,"22") + self.assertCountEqual(found.documents, [rfc], "Should ignore versions when fuzzyfinding RFCs" ) def test_fuzzy_find_documents(self): diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py index 791339c4fc5..d93c9f45313 100644 --- a/ietf/doc/utils.py +++ b/ietf/doc/utils.py @@ -1209,14 +1209,15 @@ def fuzzy_find_documents(name, rev=None): if name.startswith("rfc"): sought_type = "rfc" - log.assertion("rev is None") + name = name.split("-")[0] # strip any noise (like a revision) at and after the first hyphen + rev = None # If someone is looking for an RFC and supplies a version, ignore it. else: sought_type = "draft" # see if we can find a document using this name docs = Document.objects.filter(name=name, type_id=sought_type) - if rev and not docs.exists(): - # No document found, see if the name/rev split has been misidentified. + if sought_type == "draft" and rev and not docs.exists(): + # No draft found, see if the name/rev split has been misidentified. # Handles some special cases, like draft-ietf-tsvwg-ieee-802-11. name = '%s-%s' % (name, rev) docs = Document.objects.filter(name=name, type_id='draft') From e130a301def95e6a5ef9f6d06bad42e7443b38fc Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 13 Dec 2023 13:14:38 -0600 Subject: [PATCH 2/8] fix: gather interesting years for stats accounting for published rfcs --- ietf/stats/views.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/ietf/stats/views.py b/ietf/stats/views.py index e2b7706a26c..23e90ea23c4 100644 --- a/ietf/stats/views.py +++ b/ietf/stats/views.py @@ -14,7 +14,7 @@ from django.conf import settings from django.contrib.auth.decorators import login_required from django.core.cache import cache -from django.db.models import Count, Q +from django.db.models import Count, Q, Subquery, OuterRef from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, render from django.urls import reverse as urlreverse @@ -34,7 +34,7 @@ from ietf.person.models import Person from ietf.name.models import ReviewResultName, CountryName, DocRelationshipName, ReviewAssignmentStateName from ietf.person.name import plain_name -from ietf.doc.models import Document, State, DocEvent +from ietf.doc.models import Document, RelatedDocument, State, DocEvent from ietf.meeting.models import Meeting from ietf.stats.models import MeetingRegistration, CountryAlias from ietf.stats.utils import get_aliased_affiliations, get_aliased_countries, compute_hirsch_index @@ -607,15 +607,31 @@ def build_document_stats_url(stats_type_override=Ellipsis, get_overrides=None): doc_years = defaultdict(set) - docevent_qs = DocEvent.objects.filter( + draftevent_qs = DocEvent.objects.filter( doc__type="draft", - type__in=["published_rfc", "new_revision"], - ).values_list("doc", "time").order_by("doc") + type = "new_revision", + ).values_list("doc","time").order_by("doc") - for doc_id, time in docevent_qs.iterator(): + for doc_id, time in draftevent_qs.iterator(): # RPC_TZINFO is used to match the timezone handling in Document.pub_date() doc_years[doc_id].add(time.astimezone(RPC_TZINFO).year) + rfcevent_qs = ( + DocEvent.objects.filter(doc__type="rfc", type="published_rfc") + .annotate( + draft=Subquery( + RelatedDocument.objects.filter( + target=OuterRef("doc__pk"), relationship_id="became_rfc" + ).values_list("source", flat=True)[:1] + ) + ) + .values_list("draft", "time") + .order_by("draft") + ) + + for doc_id, time in rfcevent_qs.iterator(): + doc_years[doc_id].add(time.astimezone(RPC_TZINFO).year) + person_qs = Person.objects.filter(person_filters) if document_type == "rfc": From 6083205cc8068234bc9a92b3ff66a8a5f36a9d74 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Thu, 14 Dec 2023 11:29:55 -0400 Subject: [PATCH 3/8] fix: Update group dependencies for new rfc model (#6773) * fix: Avoid accessing slug when state is None * fix: ignore pre-rfc drafts as rfc-to-rfc references * fix: also ignore pre-rfc to pre-rfc refs * chore: remove breakpoint * fix: ignore subseries non-downrefs * fix: label nodes as RFCS when possible * fix: get "rfc" flag right * chore: Remove comment, answer seems to be "no" * refactor: Specify state type; drop redundant source doc type check We're only admitting source__type="draft", so no need to check it again in filter queries * refactor: Specify state type in old code --- ietf/group/views.py | 46 ++++++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/ietf/group/views.py b/ietf/group/views.py index f2abe73caab..991a1b8d879 100644 --- a/ietf/group/views.py +++ b/ietf/group/views.py @@ -740,14 +740,31 @@ def dependencies(request, acronym, group_type=None): source__type="draft", relationship__slug__startswith="ref", ) - - both_rfcs = Q(source__type_id="rfc", target__type_id="rfc") - inactive = Q(source__states__slug__in=["expired", "repl"]) + rfc_or_subseries = {"rfc", "bcp", "fyi", "std"} + both_rfcs = Q(source__type_id="rfc", target__type_id__in=rfc_or_subseries) + pre_rfc_draft_to_rfc = Q( + source__states__type="draft", + source__states__slug="rfc", + target__type_id__in=rfc_or_subseries, + ) + both_pre_rfcs = Q( + source__states__type="draft", + source__states__slug="rfc", + target__type_id="draft", + target__states__type="draft", + target__states__slug="rfc", + ) + inactive = Q( + source__states__type="draft", + source__states__slug__in=["expired", "repl"], + ) attractor = Q(target__name__in=["rfc5000", "rfc5741"]) - removed = Q(source__states__slug__in=["auth-rm", "ietf-rm"]) + removed = Q(source__states__type="draft", source__states__slug__in=["auth-rm", "ietf-rm"]) relations = ( RelatedDocument.objects.filter(references) .exclude(both_rfcs) + .exclude(pre_rfc_draft_to_rfc) + .exclude(both_pre_rfcs) .exclude(inactive) .exclude(attractor) .exclude(removed) @@ -755,8 +772,8 @@ def dependencies(request, acronym, group_type=None): links = set() for x in relations: - target_state = x.target.get_state_slug("draft") - if target_state != "rfc" or x.is_downref(): + always_include = x.target.type_id not in rfc_or_subseries and x.target.get_state_slug("draft") != "rfc" + if always_include or x.is_downref(): links.add(x) replacements = RelatedDocument.objects.filter( @@ -771,13 +788,12 @@ def dependencies(request, acronym, group_type=None): graph = { "nodes": [ { - "id": x.name, - "rfc": x.get_state("draft").slug == "rfc", - "post-wg": not x.get_state("draft-iesg").slug - in ["idexists", "watching", "dead"], - "expired": x.get_state("draft").slug == "expired", - "replaced": x.get_state("draft").slug == "repl", - "group": x.group.acronym if x.group.acronym != "none" else "", + "id": x.became_rfc().name if x.became_rfc() else x.name, + "rfc": x.type_id == "rfc" or x.became_rfc() is not None, + "post-wg": x.get_state_slug("draft-iesg") not in ["idexists", "watching", "dead"], + "expired": x.get_state_slug("draft") == "expired", + "replaced": x.get_state_slug("draft") == "repl", + "group": x.group.acronym if x.group and x.group.acronym != "none" else "", "url": x.get_absolute_url(), "level": x.intended_std_level.name if x.intended_std_level @@ -789,8 +805,8 @@ def dependencies(request, acronym, group_type=None): ], "links": [ { - "source": x.source.name, - "target": x.target.name, + "source": x.source.became_rfc().name if x.source.became_rfc() else x.source.name, + "target": x.target.became_rfc().name if x.target.became_rfc() else x.target.name, "rel": "downref" if x.is_downref() else x.relationship.slug, } for x in links From 501a5b8e5816c6be84853dfdcb9dcdfc747bafb1 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 14 Dec 2023 09:35:30 -0600 Subject: [PATCH 4/8] fix: consider subseries when calculating downref (#6777) --- ietf/doc/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ietf/doc/models.py b/ietf/doc/models.py index 4d7a4f805cc..fdda23ecd14 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -700,6 +700,8 @@ 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"]: + source_lvl = self.source.type_id else: source_lvl = self.source.intended_std_level_id @@ -711,6 +713,8 @@ def is_downref(self): 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' From e9d44d41f096979286a65c6cd12492cb5b37b82a Mon Sep 17 00:00:00 2001 From: Ryan Cross Date: Thu, 14 Dec 2023 11:46:27 -0800 Subject: [PATCH 5/8] feat: Capture volunteers from the registration system (#6605) * feat: Capture volunteers from the registration system. Fixes #5938 * fix: allow withdrawn field to be null * docs: add explanatory code comments * fix: identify current nomcom by is_accepting_volunteers * fix: use auto_now_add instead of auto_now * fix: update migration --------- Co-authored-by: Robert Sparks --- ietf/api/tests.py | 47 +++++++++++++++++++ ietf/api/views.py | 17 ++++++- ...igin_volunteer_time_volunteer_withdrawn.py | 27 +++++++++++ ietf/nomcom/models.py | 5 +- 4 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 ietf/nomcom/migrations/0004_volunteer_origin_volunteer_time_volunteer_withdrawn.py diff --git a/ietf/api/tests.py b/ietf/api/tests.py index e61069b3f59..3d3e3ac1211 100644 --- a/ietf/api/tests.py +++ b/ietf/api/tests.py @@ -28,6 +28,8 @@ from ietf.group.factories import RoleFactory from ietf.meeting.factories import MeetingFactory, SessionFactory from ietf.meeting.models import Session +from ietf.nomcom.models import Volunteer, NomCom +from ietf.nomcom.factories import NomComFactory, nomcom_kwargs_for_year from ietf.person.factories import PersonFactory, random_faker from ietf.person.models import User from ietf.person.models import PersonalApiKey @@ -630,6 +632,7 @@ def test_api_new_meeting_registration(self): 'reg_type': 'hackathon', 'ticket_type': '', 'checkedin': 'False', + 'is_nomcom_volunteer': 'False', } url = urlreverse('ietf.api.views.api_new_meeting_registration') r = self.client.post(url, reg) @@ -691,6 +694,50 @@ def test_api_new_meeting_registration(self): 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') + reg = { + 'apikey': 'invalid', + 'affiliation': "Alguma Corporação", + 'country_code': 'PT', + 'meeting': meeting.number, + 'reg_type': 'onsite', + 'ticket_type': '', + 'checkedin': 'False', + 'is_nomcom_volunteer': 'True', + } + person = PersonFactory() + 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') + r = self.client.post(url, reg) + 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') + key = PersonalApiKey.objects.create(person=oidcp, endpoint=url) + reg['apikey'] = key.hash() + r = self.client.post(url, reg) + nomcom = NomCom.objects.last() + self.assertContains(r, "Accepted, New registration", status_code=202) + # assert Volunteer exists + self.assertEqual(Volunteer.objects.count(), 1) + volunteer = Volunteer.objects.last() + self.assertEqual(volunteer.person, person) + self.assertEqual(volunteer.nomcom, nomcom) + 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') diff --git a/ietf/api/views.py b/ietf/api/views.py index 9d832f6fa58..e587b371211 100644 --- a/ietf/api/views.py +++ b/ietf/api/views.py @@ -1,7 +1,6 @@ # Copyright The IETF Trust 2017-2020, All Rights Reserved # -*- coding: utf-8 -*- - import json import pytz import re @@ -38,6 +37,7 @@ from ietf.ietfauth.views import send_account_creation_email from ietf.ietfauth.utils import role_required from ietf.meeting.models import Meeting +from ietf.nomcom.models import Volunteer, NomCom from ietf.stats.models import MeetingRegistration from ietf.utils import log from ietf.utils.decorators import require_api_key @@ -140,7 +140,7 @@ def api_new_meeting_registration(request): 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'] + 'email', 'reg_type', 'ticket_type', 'checkedin', 'is_nomcom_volunteer'] fields = required_fields + [] if request.method == 'POST': # parameters: @@ -202,6 +202,19 @@ def err(code, text): else: send_account_creation_email(request, email) response += ", Email sent" + + # handle nomcom volunteer + if data['is_nomcom_volunteer'] and object.person: + try: + nomcom = NomCom.objects.get(is_accepting_volunteers=True) + except (NomCom.DoesNotExist, NomCom.MultipleObjectsReturned): + nomcom = None + if nomcom: + Volunteer.objects.create( + nomcom=nomcom, + person=object.person, + affiliation=data['affiliation'], + origin='registration') return HttpResponse(response, status=202, content_type='text/plain') else: return HttpResponse(status=405) diff --git a/ietf/nomcom/migrations/0004_volunteer_origin_volunteer_time_volunteer_withdrawn.py b/ietf/nomcom/migrations/0004_volunteer_origin_volunteer_time_volunteer_withdrawn.py new file mode 100644 index 00000000000..9eaebf2069d --- /dev/null +++ b/ietf/nomcom/migrations/0004_volunteer_origin_volunteer_time_volunteer_withdrawn.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.7 on 2023-11-05 09:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("nomcom", "0003_alter_nomination_share_nominator"), + ] + + operations = [ + migrations.AddField( + model_name="volunteer", + name="origin", + field=models.CharField(default="datatracker", max_length=32), + ), + migrations.AddField( + model_name="volunteer", + name="time", + field=models.DateTimeField(auto_now_add=True, null=True, blank=True), + ), + migrations.AddField( + model_name="volunteer", + name="withdrawn", + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/ietf/nomcom/models.py b/ietf/nomcom/models.py index ee2eea2ccaf..51006a227ee 100644 --- a/ietf/nomcom/models.py +++ b/ietf/nomcom/models.py @@ -327,7 +327,10 @@ class Volunteer(models.Model): nomcom = ForeignKey('NomCom') person = ForeignKey(Person) affiliation = models.CharField(blank=True, max_length=255) - + time = models.DateTimeField(auto_now_add=True, null=True, blank=True) + origin = models.CharField(max_length=32, default='datatracker') + withdrawn = models.DateTimeField(blank=True, null=True) + def __str__(self): return f'{self.person} for {self.nomcom}' From cfaf84f08f72669d38b565b5648ed33876139ce1 Mon Sep 17 00:00:00 2001 From: Paul Selkirk Date: Thu, 14 Dec 2023 15:19:22 -0500 Subject: [PATCH 6/8] refactor: Convert custom form template to bootstrap (#6763) * refactor: Convert custom form template to bootstrap * refactor: Match form fields to old version --- ietf/meeting/forms.py | 10 ++++++-- .../meeting/interim_send_announcement.html | 23 ++----------------- 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/ietf/meeting/forms.py b/ietf/meeting/forms.py index 84853b83e99..ef6a2721e99 100644 --- a/ietf/meeting/forms.py +++ b/ietf/meeting/forms.py @@ -1,4 +1,4 @@ -# Copyright The IETF Trust 2016-2020, All Rights Reserved +# Copyright The IETF Trust 2016-2023, All Rights Reserved # -*- coding: utf-8 -*- @@ -360,7 +360,13 @@ def save_agenda(self): class InterimAnnounceForm(forms.ModelForm): class Meta: model = Message - fields = ('to', 'frm', 'cc', 'bcc', 'reply_to', 'subject', 'body') + fields = ('to', 'cc', 'frm', 'subject', 'body') + + def __init__(self, *args, **kwargs): + super(InterimAnnounceForm, self).__init__(*args, **kwargs) + self.fields['frm'].label='From' + self.fields['frm'].widget.attrs['readonly'] = True + self.fields['to'].widget.attrs['readonly'] = True def save(self, *args, **kwargs): user = kwargs.pop('user') diff --git a/ietf/templates/meeting/interim_send_announcement.html b/ietf/templates/meeting/interim_send_announcement.html index 3e08f7ee0de..d54229200ac 100644 --- a/ietf/templates/meeting/interim_send_announcement.html +++ b/ietf/templates/meeting/interim_send_announcement.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{# Copyright The IETF Trust 2015, All Rights Reserved #} +{# Copyright The IETF Trust 2015-2023, All Rights Reserved #} {% load origin %} {% load static django_bootstrap5 widget_tweaks %} {% block title %}Announce Interim Meeting{% endblock %} @@ -11,26 +11,7 @@

Announce Interim Meeting

{% csrf_token %} -
- -
{% render_field form.to class="form-control" readonly="readonly" %}
-
-
- -
{% render_field form.cc class="form-control" %}
-
-
- -
{% render_field form.frm class="form-control" readonly="readonly" %}
-
-
- -
{% render_field form.subject class="form-control" %}
-
-
- -
{% render_field form.body class="form-control" %}
-
+ {% bootstrap_form form layout="horizontal" %} From 4216c129fe55cfa2c529bf273128fbf1e870f78c Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 14 Dec 2023 15:04:27 -0600 Subject: [PATCH 7/8] fix: change verbosity of api key purge mgmt command (#6758) * fix: change verbosity of api key purge mgmt command * test: adjust mgmt command test arguments --- .../purge_old_personal_api_key_events.py | 23 +++++++++++-------- ietf/person/management/commands/tests.py | 10 ++++---- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/ietf/person/management/commands/purge_old_personal_api_key_events.py b/ietf/person/management/commands/purge_old_personal_api_key_events.py index a32edf866c1..66b9d2c33ec 100644 --- a/ietf/person/management/commands/purge_old_personal_api_key_events.py +++ b/ietf/person/management/commands/purge_old_personal_api_key_events.py @@ -18,9 +18,11 @@ def add_arguments(self, parser): parser.add_argument('-n', '--dry-run', action='store_true', default=False, help="Don't delete events, just show what would be done") + def handle(self, *args, **options): keep_days = options['keep_days'] dry_run = options['dry_run'] + verbosity = options.get("verbosity", 1) def _format_count(count, unit='day'): return '{} {}{}'.format(count, unit, ('' if count == 1 else 's')) @@ -28,10 +30,11 @@ def _format_count(count, unit='day'): if keep_days < 0: raise CommandError('Negative keep_days not allowed ({} was specified)'.format(keep_days)) - self.stdout.write('purge_old_personal_api_key_events: Finding events older than {}\n'.format(_format_count(keep_days))) - if dry_run: - self.stdout.write('Dry run requested, records will not be deleted\n') - self.stdout.flush() + if verbosity > 1: + self.stdout.write('purge_old_personal_api_key_events: Finding events older than {}\n'.format(_format_count(keep_days))) + if dry_run: + self.stdout.write('Dry run requested, records will not be deleted\n') + self.stdout.flush() now = timezone.now() old_events = PersonApiKeyEvent.objects.filter( @@ -41,7 +44,8 @@ def _format_count(count, unit='day'): stats = old_events.aggregate(Min('time'), Max('time')) old_count = old_events.count() if old_count == 0: - self.stdout.write('No events older than {} found\n'.format(_format_count(keep_days))) + if verbosity > 1: + self.stdout.write('No events older than {} found\n'.format(_format_count(keep_days))) return oldest_date = stats['time__min'] @@ -50,10 +54,11 @@ def _format_count(count, unit='day'): newest_ago = now - newest_date action_fmt = 'Would delete {}\n' if dry_run else 'Deleting {}\n' - self.stdout.write(action_fmt.format(_format_count(old_count, 'event'))) - self.stdout.write(' Oldest at {} ({} ago)\n'.format(oldest_date, _format_count(oldest_ago.days))) - self.stdout.write(' Most recent at {} ({} ago)\n'.format(newest_date, _format_count(newest_ago.days))) - self.stdout.flush() + if verbosity > 1: + self.stdout.write(action_fmt.format(_format_count(old_count, 'event'))) + self.stdout.write(' Oldest at {} ({} ago)\n'.format(oldest_date, _format_count(oldest_ago.days))) + self.stdout.write(' Most recent at {} ({} ago)\n'.format(newest_date, _format_count(newest_ago.days))) + self.stdout.flush() if not dry_run: old_events.delete() diff --git a/ietf/person/management/commands/tests.py b/ietf/person/management/commands/tests.py index 291a6ace5f1..38d770a588b 100644 --- a/ietf/person/management/commands/tests.py +++ b/ietf/person/management/commands/tests.py @@ -76,26 +76,26 @@ def test_purge_old_personal_api_key_events(self): num_recent_events = len(recent_events) # call with dry run - output = self._call_command('purge_old_personal_api_key_events', str(keep_days), '--dry-run') + output = self._call_command('purge_old_personal_api_key_events', str(keep_days), '--dry-run', '-v2') self._assert_purge_dry_run_results(output, num_old_events, old_events + recent_events) # call for real - output = self._call_command('purge_old_personal_api_key_events', str(keep_days)) + output = self._call_command('purge_old_personal_api_key_events', str(keep_days), '-v2') self._assert_purge_results(output, num_old_events, recent_events) self.assertEqual(PersonEvent.objects.count(), personevents_before + num_recent_events, 'PersonEvents were not cleaned up properly') # repeat - there should be nothing left to delete - output = self._call_command('purge_old_personal_api_key_events', '--dry-run', str(keep_days)) + output = self._call_command('purge_old_personal_api_key_events', '--dry-run', str(keep_days), '-v2') self._assert_purge_dry_run_results(output, 0, recent_events) - output = self._call_command('purge_old_personal_api_key_events', str(keep_days)) + output = self._call_command('purge_old_personal_api_key_events', str(keep_days), '-v2') self._assert_purge_results(output, 0, recent_events) self.assertEqual(PersonEvent.objects.count(), personevents_before + num_recent_events, 'PersonEvents were not cleaned up properly') # and now delete the remaining events - output = self._call_command('purge_old_personal_api_key_events', '0') + output = self._call_command('purge_old_personal_api_key_events', '0', '-v2') self._assert_purge_results(output, num_recent_events, []) self.assertEqual(PersonEvent.objects.count(), personevents_before, 'PersonEvents were not cleaned up properly') From f5bd0783515e8577c0f50a903eb3fd85b9048532 Mon Sep 17 00:00:00 2001 From: Lars Eggert Date: Thu, 14 Dec 2023 23:05:00 +0200 Subject: [PATCH 8/8] fix: AD queue graphs and AD doc list sorting (#6752) * Fix AD queue graphs and AD doc list sorting. Fixes #6700 Fixes #6720 * Fix the issue @rdanyliw found. Hopefully. * More fixes --------- Co-authored-by: Robert Sparks --- ietf/doc/views_search.py | 53 ++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/ietf/doc/views_search.py b/ietf/doc/views_search.py index eef57a29c14..2e4231c5ace 100644 --- a/ietf/doc/views_search.py +++ b/ietf/doc/views_search.py @@ -418,13 +418,19 @@ def state_name(doc_type, state, shorten=True): for dt in AD_WORKLOAD } + +def state_to_doc_type(state): + for dt in STATE_SLUGS: + if state in STATE_SLUGS[dt]: + return dt + return None + + IESG_STATES = State.objects.filter(type="draft-iesg").values_list("name", flat=True) def date_to_bucket(date, now, num_buckets): - return num_buckets - min( - num_buckets, int((now.date() - date.date()).total_seconds() / 60 / 60 / 24) - ) + return num_buckets - int((now.date() - date.date()).total_seconds() / 60 / 60 / 24) def ad_workload(request): @@ -477,6 +483,7 @@ def ad_workload(request): to_state = state_name(dt, state, shorten=False) elif e.desc.endswith("has been replaced"): # stop tracking + last = e.time break if not to_state: @@ -501,26 +508,30 @@ def ad_workload(request): elif to_state == "RFC Published": to_state = "RFC" + if dt == "rfc": + new_dt = state_to_doc_type(to_state) + if new_dt is not None and new_dt != dt: + dt = new_dt + if to_state not in STATE_SLUGS[dt].keys() or to_state == "Replaced": # change into a state the AD dashboard doesn't display if to_state in IESG_STATES or to_state == "Replaced": - # if it's an IESG state we don't display, we're done with this doc + # if it's an IESG state we don't display, record it's time last = e.time - break - # if it's not an IESG state, keep going with next event + # keep going with next event continue sn = STATE_SLUGS[dt][to_state] buckets_start = date_to_bucket(e.time, now, days) buckets_end = date_to_bucket(last, now, days) - if buckets_end >= days: - # this event is older than we record in the history - if last == now: - # but since we didn't record any state yet, - # this is the state the doc was in for the - # entire history - for b in range(buckets_start, days): + if dt == "charter" and to_state == "Approved" and buckets_start < 0: + # don't count old charter approvals + break + + if buckets_start <= 0: + if buckets_end >= 0: + for b in range(0, buckets_end): ad.buckets[dt][sn][b].append(doc.name) sums[dt][sn][b].append(doc.name) last = e.time @@ -532,15 +543,6 @@ def ad_workload(request): sums[dt][sn][b].append(doc.name) last = e.time - if last == now: - s = state_name(dt, state, shorten=False) - if s in STATE_SLUGS[dt].keys(): - # we didn't have a single event for this doc, assume - # the current state applied throughput the history - for b in range(days): - ad.buckets[dt][state][b].append(doc.name) - sums[dt][state][b].append(doc.name) - metadata = [ { "type": (dt, doc_type_name(dt)), @@ -564,8 +566,11 @@ def ad_workload(request): def docs_for_ad(request, name): def sort_key(doc): - key = list(AD_WORKLOAD.keys()).index(doc_type(doc)) - return key + dt = doc_type(doc) + dt_key = list(AD_WORKLOAD.keys()).index(dt) + ds = doc_state(doc) + ds_key = AD_WORKLOAD[dt].index(ds) if ds in AD_WORKLOAD[dt] else 99 + return dt_key * 100 + ds_key ad = None responsible = Document.objects.values_list("ad", flat=True).distinct()