diff --git a/.gitignore b/.gitignore index c25e6b5bfe..84bc800e3b 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ datatracker.sublime-workspace /docker/docker-compose.extend-custom.yml /env /ghostdriver.log +/geckodriver.log /htmlcov /ietf/static/dist-neue /latest-coverage.json diff --git a/ietf/doc/views_search.py b/ietf/doc/views_search.py index 2ef4ee83e6..528fb05a22 100644 --- a/ietf/doc/views_search.py +++ b/ietf/doc/views_search.py @@ -485,6 +485,29 @@ def _state_to_doc_type(state): ) ad.buckets = copy.deepcopy(bucket_template) + # https://github.com/ietf-tools/datatracker/issues/4577 + docs_via_group_ad = Document.objects.exclude( + group__acronym="none" + ).filter( + group__role__name="ad", + group__role__person=ad + ).filter( + states__type="draft-stream-ietf", + states__slug__in=["wg-doc","wg-lc","waiting-for-implementation","chair-w","writeupw"] + ) + + doc_for_ad = Document.objects.filter(ad=ad) + + ad.pre_pubreq = (docs_via_group_ad | doc_for_ad).filter( + type="draft" + ).filter( + states__type="draft", + states__slug="active" + ).filter( + states__type="draft-iesg", + states__slug="idexists" + ).distinct().count() + for doc in Document.objects.exclude(type_id="rfc").filter(ad=ad): dt = doc_type(doc) state = doc_state(doc) diff --git a/ietf/iesg/tests.py b/ietf/iesg/tests.py index 4579316f22..8438cb44dd 100644 --- a/ietf/iesg/tests.py +++ b/ietf/iesg/tests.py @@ -18,7 +18,7 @@ from ietf.doc.models import DocEvent, BallotPositionDocEvent, TelechatDocEvent from ietf.doc.models import Document, State, RelatedDocument -from ietf.doc.factories import WgDraftFactory, IndividualDraftFactory, ConflictReviewFactory, BaseDocumentFactory, CharterFactory, WgRfcFactory, IndividualRfcFactory +from ietf.doc.factories import BallotDocEventFactory, BallotPositionDocEventFactory, TelechatDocEventFactory, WgDraftFactory, IndividualDraftFactory, ConflictReviewFactory, BaseDocumentFactory, CharterFactory, WgRfcFactory, IndividualRfcFactory from ietf.doc.utils import create_ballot_if_not_open from ietf.group.factories import RoleFactory, GroupFactory, DatedGroupMilestoneFactory, DatelessGroupMilestoneFactory from ietf.group.models import Group, GroupMilestone, Role @@ -30,7 +30,6 @@ from ietf.iesg.factories import IESGMgmtItemFactory, TelechatAgendaContentFactory from ietf.utils.timezone import date_today, DEADLINE_TZINFO - class IESGTests(TestCase): def test_feed(self): draft = WgDraftFactory(states=[('draft','active'),('draft-iesg','iesg-eva')],ad=Person.objects.get(user__username='ad')) @@ -509,12 +508,13 @@ def test_agenda_documents_txt(self): def test_agenda_documents(self): url = urlreverse("ietf.iesg.views.agenda_documents") r = self.client.get(url) + self.assertEqual(r.status_code, 200) for k, d in self.telechat_docs.items(): self.assertContains(r, d.name, msg_prefix="%s '%s' not in response" % (k, d.name, )) - self.assertContains(r, d.title, msg_prefix="%s '%s' title not in response" % (k, d.title, )) - + self.assertContains(r, d.title, msg_prefix="%s '%s' not in response" % (k, d.title, )) + def test_past_documents(self): url = urlreverse("ietf.iesg.views.past_documents") # We haven't put any documents on past telechats, so this should be empty @@ -589,6 +589,66 @@ def test_admin_change(self): draft = Document.objects.get(name="draft-ietf-mars-test") self.assertEqual(draft.telechat_date(),today) +class IESGAgendaTelechatPagesTests(TestCase): + def setUp(self): + super().setUp() + # make_immutable_test_data made a set of future telechats - only need one + # We'll take the "next" one + self.telechat_date = get_agenda_date() + # make_immutable_test_data made and area with only one ad - give it another + ad = Person.objects.get(user__username="ad") + adrole = Role.objects.get(person=ad, name="ad") + ad2 = RoleFactory(group=adrole.group, name_id="ad").person + self.ads=[ad,ad2] + + # Make some drafts + docs = [ + WgDraftFactory(pages=2, states=[('draft-iesg','iesg-eva'),]), + IndividualDraftFactory(pages=20, states=[('draft-iesg','iesg-eva'),]), + WgDraftFactory(pages=200, states=[('draft-iesg','iesg-eva'),]), + ] + # Put them on the telechat + for doc in docs: + TelechatDocEventFactory(doc=doc, telechat_date=self.telechat_date) + # Give them ballots + ballots = [BallotDocEventFactory(doc=doc) for doc in docs] + + # Give the "ad" Area-Director a discuss on one + BallotPositionDocEventFactory(balloter=ad, doc=docs[0], pos_id="discuss", ballot=ballots[0]) + # and a "norecord" position on another + BallotPositionDocEventFactory(balloter=ad, doc=docs[1], pos_id="norecord", ballot=ballots[1]) + # Now "ad" should have 220 pages left to ballot on. + # Every other ad should have 222 pages left to ballot on. + + def test_ad_pages_left_to_ballot_on(self): + url = urlreverse("ietf.iesg.views.agenda_documents") + + # A non-AD user won't get "pages left" + response = self.client.get(url) + telechat = response.context["telechats"][0] + self.assertEqual(telechat["date"], self.telechat_date) + self.assertEqual(telechat["ad_pages_left_to_ballot_on"],0) + self.assertNotContains(response,"pages left to ballot on") + + username=self.ads[0].user.username + self.assertTrue(self.client.login(username=username, password=f"{username}+password")) + + response = self.client.get(url) + telechat = response.context["telechats"][0] + self.assertEqual(telechat["ad_pages_left_to_ballot_on"],220) + self.assertContains(response,"220 pages left to ballot on") + + self.client.logout() + username=self.ads[1].user.username + self.assertTrue(self.client.login(username=username, password=f"{username}+password")) + + response = self.client.get(url) + telechat = response.context["telechats"][0] + self.assertEqual(telechat["ad_pages_left_to_ballot_on"],222) + + + + class RescheduleOnAgendaTests(TestCase): def test_reschedule(self): draft = WgDraftFactory() diff --git a/ietf/iesg/utils.py b/ietf/iesg/utils.py index 3f4883798f..a56fa72cee 100644 --- a/ietf/iesg/utils.py +++ b/ietf/iesg/utils.py @@ -7,11 +7,11 @@ from ietf.iesg.agenda import get_doc_section -TelechatPageCount = namedtuple('TelechatPageCount',['for_approval','for_action','related']) +TelechatPageCount = namedtuple('TelechatPageCount',['for_approval','for_action','related','ad_pages_left_to_ballot_on']) -def telechat_page_count(date=None, docs=None): +def telechat_page_count(date=None, docs=None, ad=None): if not date and not docs: - return TelechatPageCount(0, 0, 0) + return TelechatPageCount(0, 0, 0, 0) if not docs: candidates = Document.objects.filter(docevent__telechatdocevent__telechat_date=date).distinct() @@ -24,7 +24,18 @@ def telechat_page_count(date=None, docs=None): drafts = [d for d in for_approval if d.type_id == 'draft'] - pages_for_approval = sum([d.pages or 0 for d in drafts]) + ad_pages_left_to_ballot_on = 0 + pages_for_approval = 0 + + for draft in drafts: + pages_for_approval += draft.pages or 0 + if ad: + ballot = draft.active_ballot() + if ballot: + positions = ballot.active_balloter_positions() + ad_position = positions[ad] + if ad_position is None or ad_position.pos_id == "norecord": + ad_pages_left_to_ballot_on += draft.pages or 0 pages_for_action = 0 for d in for_action: @@ -53,4 +64,5 @@ def telechat_page_count(date=None, docs=None): return TelechatPageCount(for_approval=pages_for_approval, for_action=pages_for_action, - related=related_pages) + related=related_pages, + ad_pages_left_to_ballot_on=ad_pages_left_to_ballot_on) diff --git a/ietf/iesg/views.py b/ietf/iesg/views.py index a92d617ac5..df02754f2e 100644 --- a/ietf/iesg/views.py +++ b/ietf/iesg/views.py @@ -360,6 +360,8 @@ def handle_reschedule_form(request, doc, dates, status): return form def agenda_documents(request): + ad = request.user.person if has_role(request.user, "Area Director") else None + dates = list(TelechatDate.objects.active().order_by('date').values_list("date", flat=True)[:4]) docs_by_date = dict((d, []) for d in dates) @@ -389,11 +391,13 @@ def agenda_documents(request): # the search_result_row view to display them (which expects them) fill_in_document_table_attributes(docs_by_date[date], have_telechat_date=True) fill_in_agenda_docs(date, sections, docs_by_date[date]) - pages = telechat_page_count(docs=docs_by_date[date]).for_approval - + page_count = telechat_page_count(docs=docs_by_date[date], ad=ad) + pages = page_count.for_approval + telechats.append({ "date": date, "pages": pages, + "ad_pages_left_to_ballot_on": page_count.ad_pages_left_to_ballot_on, "sections": sorted((num, section) for num, section in sections.items() if "2" <= num < "5") }) diff --git a/ietf/templates/doc/ad_list.html b/ietf/templates/doc/ad_list.html index cfc8830e50..a73264c0f3 100644 --- a/ietf/templates/doc/ad_list.html +++ b/ietf/templates/doc/ad_list.html @@ -35,9 +35,12 @@

{{ dt.type.1 }} State Counts

Area Director + {% if dt.type.1 == "Internet-Draft" %} + Pre pubreq + {% endif %} {% for state, state_name in dt.states %} - + {{ state_name|split:'/'|join:'/' }} @@ -51,6 +54,17 @@

{{ dt.type.1 }} State Counts

{{ ad.name }} + {% if dt.type.1 == "Internet-Draft" %} + + {{ ad.pre_pubreq }} + + {% endif %} {% for state, state_name in dt.states %} @@ -63,6 +77,16 @@

{{ dt.type.1 }} State Counts

Sum + {% if dt.type.1 == "Internet-Draft" %} + +
+ + {% endif %} {% for state, state_name in dt.states %}
@@ -87,37 +111,151 @@

{{ dt.type.1 }} State Counts

{{ data|json_script:"data" }} + + + + {% endblock %} \ No newline at end of file diff --git a/ietf/templates/iesg/agenda_documents.html b/ietf/templates/iesg/agenda_documents.html index 80dd9956fa..f732672df0 100644 --- a/ietf/templates/iesg/agenda_documents.html +++ b/ietf/templates/iesg/agenda_documents.html @@ -21,7 +21,12 @@

Documents on future IESG telechat agendas

IESG telechat {{ t.date }}
- {{ t.pages }} page{{ t.pages|pluralize }} + + {{ t.pages }} page{{ t.pages|pluralize }} + {% if t.ad_pages_left_to_ballot_on %} + ({{ t.ad_pages_left_to_ballot_on }} pages left to ballot on) + {% endif %} +

diff --git a/playwright/tests-legacy/docs/ad.spec.js b/playwright/tests-legacy/docs/ad.spec.js new file mode 100644 index 0000000000..80b8b27cda --- /dev/null +++ b/playwright/tests-legacy/docs/ad.spec.js @@ -0,0 +1,26 @@ +const { test, expect } = require('@playwright/test') +const viewports = require('../../helpers/viewports') + +// ==================================================================== +// IESG Dashboard +// ==================================================================== + +test.describe('/doc/ad/', () => { + test.beforeEach(async ({ page }) => { + await page.setViewportSize({ + width: viewports.desktop[0], + height: viewports.desktop[1] + }) + + await page.goto('/doc/ad/') + }) + + test('Pre pubreq', async ({ page }) => { + const tablesLocator = page.locator('table') + const tablesCount = await tablesLocator.count() + expect(tablesCount).toBeGreaterThan(0) + const firstTable = tablesLocator.nth(0) + const theadTexts = await firstTable.locator('thead').allInnerTexts() + expect(theadTexts.join('')).toContain('Pre pubreq') + }) +})