Repository: ietf-tools/datatracker

Issue:
leap day bug in get_8989_eligible_querysets method
### Describe the issue

In `ietf.nomcom.utils.get_8989_eligible_querysets()`, dates three and five years before a given date are constructed via `datetime.datetime(date.year - 3, month=date.month, day=date.day)`. This does not work if date is Feb 29 of a leap year. This should be special cased, deciding whether Feb 28 or March 1 is the appropriate choice, or should be calculated using datetime arithmetic.

The tests will need to be updated to match.

Calling this medium priority, but I don't know whether Feb 29 is a plausible eligibility date. If not, this is minor.

### Code of Conduct

- [X] I agree to follow the [IETF's Code of Conduct](https://github.com/ietf-tools/.github/blob/main/CODE_OF_CONDUCT.md)


Discussion:

Resolving patch:
diff --git a/ietf/nomcom/utils.py b/ietf/nomcom/utils.py
--- a/ietf/nomcom/utils.py
+++ b/ietf/nomcom/utils.py
@@ -538,14 +538,21 @@ def get_8989_eligibility_querysets(date, base_qs):
     previous_five = previous_five_meetings(date)
     three_of_five_qs = new_three_of_five_eligible(previous_five=previous_five, queryset=base_qs)
 
-    three_years_ago = datetime.date(date.year-3,date.month,date.day)
+    # If date is Feb 29, neither 3 nor 5 years ago has a Feb 29. Use Feb 28 instead.
+    if date.month == 2 and date.day == 29:
+        three_years_ago = datetime.date(date.year - 3, 2, 28)
+        five_years_ago = datetime.date(date.year - 5, 2, 28)
+    else:
+        three_years_ago = datetime.date(date.year - 3, date.month, date.day)
+        five_years_ago = datetime.date(date.year - 5, date.month, date.day)
+
     officer_qs = base_qs.filter(
         # is currently an officer
         Q(role__name_id__in=('chair','secr'),
           role__group__state_id='active',
           role__group__type_id='wg',
           role__group__time__lte=date,
-        ) 
+        )
         # was an officer since the given date (I think this is wrong - it looks at when roles _start_, not when roles end)
       | Q(rolehistory__group__time__gte=three_years_ago,
           rolehistory__group__time__lte=date,
@@ -555,7 +562,6 @@ def get_8989_eligibility_querysets(date, base_qs):
          )
     ).distinct()
 
-    five_years_ago = datetime.date(date.year-5,date.month,date.day)
     rfc_pks = set(DocEvent.objects.filter(type='published_rfc',time__gte=five_years_ago,time__lte=date).values_list('doc__pk',flat=True))
     iesgappr_pks = set(DocEvent.objects.filter(type='iesg_approved',time__gte=five_years_ago,time__lte=date).values_list('doc__pk',flat=True))
     qualifying_pks = rfc_pks.union(iesgappr_pks.difference(rfc_pks))


Test patch:
diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py
--- a/ietf/nomcom/tests.py
+++ b/ietf/nomcom/tests.py
@@ -2452,8 +2452,13 @@ def test_elig_by_office_closed_groups(self):
         for nomcom in self.nomcoms:
             elig_date=get_eligibility_date(nomcom)
             day_before = elig_date-datetime.timedelta(days=1)
-            year_before = datetime.date(elig_date.year-1,elig_date.month,elig_date.day)
-            three_years_before = datetime.date(elig_date.year-3,elig_date.month,elig_date.day)
+            # special case for Feb 29
+            if elig_date.month == 2 and elig_date.day == 29:
+                year_before = datetime.date(elig_date.year - 1, 2, 28)
+                three_years_before = datetime.date(elig_date.year - 3, 2, 28)
+            else:
+                year_before = datetime.date(elig_date.year - 1, elig_date.month, elig_date.day)
+                three_years_before = datetime.date(elig_date.year - 3, elig_date.month, elig_date.day)
             just_after_three_years_before = three_years_before + datetime.timedelta(days=1)
             just_before_three_years_before = three_years_before - datetime.timedelta(days=1)
 
@@ -2513,10 +2518,15 @@ def test_elig_by_author(self):
             elig_date = get_eligibility_date(nomcom)
 
             last_date = elig_date
-            first_date = datetime.date(last_date.year-5,last_date.month,last_date.day)
+            # special case for Feb 29
+            if last_date.month == 2 and last_date.day == 29:
+                first_date = datetime.date(last_date.year - 5, 2, 28)
+                middle_date = datetime.date(last_date.year - 3, 2, 28)
+            else:
+                first_date = datetime.date(last_date.year - 5, last_date.month, last_date.day)
+                middle_date = datetime.date(last_date.year - 3, last_date.month, last_date.day)
             day_after_last_date = last_date+datetime.timedelta(days=1)
             day_before_first_date = first_date-datetime.timedelta(days=1)
-            middle_date = datetime.date(last_date.year-3,last_date.month,last_date.day)
 
             eligible = set()
             ineligible = set()
@@ -2663,7 +2673,14 @@ def test_decorate_volunteers_with_qualifications(self):
         author_person = PersonFactory()
         for i in range(2):
             da = WgDocumentAuthorFactory(person=author_person)
-            DocEventFactory(type='published_rfc',doc=da.document,time=datetime.date(elig_date.year-3,elig_date.month,elig_date.day))
+            DocEventFactory(
+                type='published_rfc',
+                doc=da.document,
+                time=datetime.date(
+                    elig_date.year - 3,
+                    elig_date.month,
+                    28 if elig_date.month == 2 and elig_date.day == 29 else elig_date.day,
+                ))
         nomcom.volunteer_set.create(person=author_person)
 
         volunteers = nomcom.volunteer_set.all()