We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Wrapping issues in ballot modal on narrow screens
### Describe the issue

<img width="582" alt="Screenshot 2022-12-15 at 18 02 42" src="https://user-images.githubusercontent.com/200328/207908976-51568fb5-a3b4-4ccc-8026-8065d13da38d.png">


### 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)

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `ietf/doc/templatetags/ballot_icon.py`
Content:
```
1 # Copyright The IETF Trust 2012-2021, All Rights Reserved
2 # Copyright (C) 2009-2010 Nokia Corporation and/or its subsidiary(-ies).
3 # All rights reserved. Contact: Pasi Eronen <pasi.eronen@nokia.com>
4 #
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions
7 # are met:
8 #
9 #  * Redistributions of source code must retain the above copyright
10 #    notice, this list of conditions and the following disclaimer.
11 #
12 #  * Redistributions in binary form must reproduce the above
13 #    copyright notice, this list of conditions and the following
14 #    disclaimer in the documentation and/or other materials provided
15 #    with the distribution.
16 #
17 #  * Neither the name of the Nokia Corporation and/or its
18 #    subsidiary(-ies) nor the names of its contributors may be used
19 #    to endorse or promote products derived from this software
20 #    without specific prior written permission.
21 #
22 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
25 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
26 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
27 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
28 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
29 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
30 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
31 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33 
34 import datetime
35 
36 import debug      # pyflakes:ignore
37 
38 from django import template
39 from django.urls import reverse as urlreverse
40 from django.db.models import Q
41 from django.utils import timezone
42 from django.utils.safestring import mark_safe
43 
44 from ietf.ietfauth.utils import user_is_person, has_role
45 from ietf.doc.models import BallotPositionDocEvent, IESG_BALLOT_ACTIVE_STATES
46 from ietf.name.models import BallotPositionName
47 
48 
49 register = template.Library()
50 
51 @register.filter
52 def showballoticon(doc):
53     if doc.type_id == "draft":
54         if doc.stream_id == 'ietf' and doc.get_state_slug("draft-iesg") not in IESG_BALLOT_ACTIVE_STATES:
55             return False
56         elif doc.stream_id == 'irtf' and doc.get_state_slug("draft-stream-irtf") not in ['irsgpoll']:
57             return False
58     elif doc.type_id == "charter":
59         if doc.get_state_slug() not in ("intrev", "extrev", "iesgrev"):
60             return False
61     elif doc.type_id == "conflrev":
62        if doc.get_state_slug() not in ("iesgeval","defer"):
63            return False
64     elif doc.type_id == "statchg":
65        if doc.get_state_slug() not in ("iesgeval","defer", "in-lc"):
66            return False
67 
68     return True
69 
70 @register.simple_tag(takes_context=True)
71 def ballot_icon(context, doc):
72     user = context.get("user")
73 
74     if not doc:
75         return ""
76 
77     if not showballoticon(doc):
78         return ""
79 
80     ballot = doc.ballot if hasattr(doc, 'ballot') else doc.active_ballot()
81 
82     if not ballot:
83         return ""
84 
85     def sort_key(t):
86         _, pos = t
87         if not pos:
88             return (2, 0)
89         elif pos.pos.blocking:
90             return (0, pos.pos.order)
91         else:
92             return (1, pos.pos.order)
93 
94     positions = list(ballot.active_balloter_positions().items())
95     positions.sort(key=sort_key)
96 
97     right_click_string = ''
98     if has_role(user, "Area Director"):
99         right_click_string = 'oncontextmenu="window.location.href=\'%s\';return false;"' %  urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=doc.name, ballot_id=ballot.pk))
100 
101     my_blocking = False
102     for i, (balloter, pos) in enumerate(positions):
103         if user_is_person(user,balloter) and pos and pos.pos.blocking:
104             my_blocking = True
105             break
106 
107     typename = "Unknown"
108     if ballot.ballot_type.slug=='irsg-approve':
109         typename = "IRSG"
110     else:
111         typename = "IESG"
112 
113     res = ['<a %s href="%s" data-bs-toggle="modal" data-bs-target="#modal-%d" aria-label="%s positions" title="%s positions (click to show more)" class="ballot-icon"><table' % (
114             right_click_string,
115             urlreverse("ietf.doc.views_doc.ballot_popup", kwargs=dict(name=doc.name, ballot_id=ballot.pk)),
116             ballot.pk,
117             typename,
118             typename,)]
119     if my_blocking:
120         res.append(' class="is-blocking" ')
121     res.append('><tbody>')
122 
123     res.append("<tr>")
124 
125     for i, (ad, pos) in enumerate(positions):
126         # The IRSG has many more members than the IESG, so make the table wider
127         if i > 0 and i % (5 if len(positions) <= 15 else 10) == 0:
128             res.append("</tr><tr>")
129 
130         c = "position-%s" % (pos.pos.slug if pos else "norecord")
131 
132         if user_is_person(user, ad):
133             c += " my"
134 
135         res.append('<td class="%s"></td>' % c)
136 
137     # add sufficient table calls to last row to avoid HTML validation warning
138     while (i + 1) % 5 != 0:
139         res.append('<td class="position-empty"></td>')
140         i = i + 1
141 
142     res.append("</tr></tbody></table></a>")
143     res.append('<div id="modal-%d" class="modal fade" tabindex="-1" role="dialog" aria-hidden="true"><div class="modal-dialog modal-dialog-scrollable modal-xl"><div class="modal-content"></div></div></div>' % ballot.pk)
144 
145     return mark_safe("".join(res))
146 
147 @register.filter
148 def ballotposition(doc, user):
149     if not showballoticon(doc) or not has_role(user, "Area Director"):
150         return None
151 
152     ballot = doc.active_ballot()
153     if not ballot:
154         return None
155 
156     changed_pos = doc.latest_event(BallotPositionDocEvent, type="changed_ballot_position", balloter__user=user, ballot=ballot)
157     if changed_pos:
158         pos = changed_pos.pos
159     else:
160         pos = BallotPositionName.objects.get(slug="norecord")
161     return pos
162 
163 
164 @register.filter
165 def state_age_colored(doc):
166     if doc.type_id == "draft":
167         if not doc.get_state_slug() in ["active", "rfc"]:
168             # Don't show anything for expired/withdrawn/replaced drafts
169             return ""
170         iesg_state = doc.get_state_slug("draft-iesg")
171         if not iesg_state:
172             return ""
173 
174         if iesg_state in ["dead", "watching", "pub", "idexists"]:
175             return ""
176         try:
177             state_datetime = (
178                 doc.docevent_set.filter(
179                     Q(type="started_iesg_process")
180                     | Q(type="changed_state", statedocevent__state_type="draft-iesg")
181                 )
182                 .order_by("-time")[0]
183                 .time
184             )
185         except IndexError:
186             state_datetime = datetime.datetime(1990, 1, 1, tzinfo=datetime.timezone.utc)
187         days = (timezone.now() - state_datetime).days
188         # loosely based on
189         # https://trac.ietf.org/trac/iesg/wiki/PublishPath
190         if iesg_state == "lc":
191             goal1 = 30
192             goal2 = 30
193         elif iesg_state == "rfcqueue":
194             goal1 = 60
195             goal2 = 120
196         elif iesg_state in ["lc-req", "ann"]:
197             goal1 = 4
198             goal2 = 7
199         elif "need-rev" in [x.slug for x in doc.tags.all()]:
200             goal1 = 14
201             goal2 = 28
202         elif iesg_state == "pub-req":
203             goal1 = 7
204             goal2 = 14
205         elif iesg_state == "ad-eval":
206             goal1 = 14
207             goal2 = 28
208         else:
209             goal1 = 14
210             goal2 = 28
211         if days > goal2:
212             class_name = "bg-danger"
213         elif days > goal1:
214             class_name = "bg-warning"
215         else:
216             # don't show a badge when things are in the green; clutters display
217             # class_name = "text-success"
218             return ""
219         if days > goal1:
220             title = ' title="In state for %d day%s; goal is &lt;%d days."' % (
221                 days,
222                 "s" if days != 1 else "",
223                 goal1,
224             )
225         else:
226             title = ""
227         return mark_safe(
228             '<span class="badge rounded-pill %s" %s><i class="bi bi-clock-fill"></i> %d</span>'
229             % (class_name, title, days)
230         )
231     else:
232         return ""
233 
234 
235 @register.filter
236 def auth48_alert_badge(doc):
237     """Return alert badge, if any, for a document"""
238     if doc.type_id != 'draft':
239         return ''
240 
241     iesg_state = doc.get_state_slug('draft-iesg')
242     if iesg_state != 'rfcqueue':
243         return ''
244 
245     rfced_state = doc.get_state_slug('draft-rfceditor')
246     if rfced_state == 'auth48':
247         return mark_safe('<span class="badge rounded-pill bg-info" title="AUTH48">AUTH48</span>')
248 
249     return ''
250
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/ietf/doc/templatetags/ballot_icon.py b/ietf/doc/templatetags/ballot_icon.py
--- a/ietf/doc/templatetags/ballot_icon.py
+++ b/ietf/doc/templatetags/ballot_icon.py
@@ -140,7 +140,7 @@
         i = i + 1
 
     res.append("</tr></tbody></table></a>")
-    res.append('<div id="modal-%d" class="modal fade" tabindex="-1" role="dialog" aria-hidden="true"><div class="modal-dialog modal-dialog-scrollable modal-xl"><div class="modal-content"></div></div></div>' % ballot.pk)
+    res.append('<div id="modal-%d" class="modal fade" tabindex="-1" role="dialog" aria-hidden="true"><div class="modal-dialog modal-dialog-scrollable modal-xl modal-fullscreen-lg-down"><div class="modal-content"></div></div></div>' % ballot.pk)
 
     return mark_safe("".join(res))

{"golden_diff": "diff --git a/ietf/doc/templatetags/ballot_icon.py b/ietf/doc/templatetags/ballot_icon.py\n--- a/ietf/doc/templatetags/ballot_icon.py\n+++ b/ietf/doc/templatetags/ballot_icon.py\n@@ -140,7 +140,7 @@\n         i = i + 1\n \n     res.append(\"</tr></tbody></table></a>\")\n-    res.append('<div id=\"modal-%d\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\"><div class=\"modal-dialog modal-dialog-scrollable modal-xl\"><div class=\"modal-content\"></div></div></div>' % ballot.pk)\n+    res.append('<div id=\"modal-%d\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\"><div class=\"modal-dialog modal-dialog-scrollable modal-xl modal-fullscreen-lg-down\"><div class=\"modal-content\"></div></div></div>' % ballot.pk)\n \n     return mark_safe(\"\".join(res))\n", "issue": "Wrapping issues in ballot modal on narrow screens\n### Describe the issue\n\n<img width=\"582\" alt=\"Screenshot 2022-12-15 at 18 02 42\" src=\"https://user-images.githubusercontent.com/200328/207908976-51568fb5-a3b4-4ccc-8026-8065d13da38d.png\">\r\n\n\n### Code of Conduct\n\n- [X] I agree to follow the [IETF's Code of Conduct](https://github.com/ietf-tools/.github/blob/main/CODE_OF_CONDUCT.md)\n", "before_files": [{"content": "# Copyright The IETF Trust 2012-2021, All Rights Reserved\n# Copyright (C) 2009-2010 Nokia Corporation and/or its subsidiary(-ies).\n# All rights reserved. Contact: Pasi Eronen <pasi.eronen@nokia.com>\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions\n# are met:\n#\n#  * Redistributions of source code must retain the above copyright\n#    notice, this list of conditions and the following disclaimer.\n#\n#  * Redistributions in binary form must reproduce the above\n#    copyright notice, this list of conditions and the following\n#    disclaimer in the documentation and/or other materials provided\n#    with the distribution.\n#\n#  * Neither the name of the Nokia Corporation and/or its\n#    subsidiary(-ies) nor the names of its contributors may be used\n#    to endorse or promote products derived from this software\n#    without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nimport datetime\n\nimport debug      # pyflakes:ignore\n\nfrom django import template\nfrom django.urls import reverse as urlreverse\nfrom django.db.models import Q\nfrom django.utils import timezone\nfrom django.utils.safestring import mark_safe\n\nfrom ietf.ietfauth.utils import user_is_person, has_role\nfrom ietf.doc.models import BallotPositionDocEvent, IESG_BALLOT_ACTIVE_STATES\nfrom ietf.name.models import BallotPositionName\n\n\nregister = template.Library()\n\n@register.filter\ndef showballoticon(doc):\n    if doc.type_id == \"draft\":\n        if doc.stream_id == 'ietf' and doc.get_state_slug(\"draft-iesg\") not in IESG_BALLOT_ACTIVE_STATES:\n            return False\n        elif doc.stream_id == 'irtf' and doc.get_state_slug(\"draft-stream-irtf\") not in ['irsgpoll']:\n            return False\n    elif doc.type_id == \"charter\":\n        if doc.get_state_slug() not in (\"intrev\", \"extrev\", \"iesgrev\"):\n            return False\n    elif doc.type_id == \"conflrev\":\n       if doc.get_state_slug() not in (\"iesgeval\",\"defer\"):\n           return False\n    elif doc.type_id == \"statchg\":\n       if doc.get_state_slug() not in (\"iesgeval\",\"defer\", \"in-lc\"):\n           return False\n\n    return True\n\n@register.simple_tag(takes_context=True)\ndef ballot_icon(context, doc):\n    user = context.get(\"user\")\n\n    if not doc:\n        return \"\"\n\n    if not showballoticon(doc):\n        return \"\"\n\n    ballot = doc.ballot if hasattr(doc, 'ballot') else doc.active_ballot()\n\n    if not ballot:\n        return \"\"\n\n    def sort_key(t):\n        _, pos = t\n        if not pos:\n            return (2, 0)\n        elif pos.pos.blocking:\n            return (0, pos.pos.order)\n        else:\n            return (1, pos.pos.order)\n\n    positions = list(ballot.active_balloter_positions().items())\n    positions.sort(key=sort_key)\n\n    right_click_string = ''\n    if has_role(user, \"Area Director\"):\n        right_click_string = 'oncontextmenu=\"window.location.href=\\'%s\\';return false;\"' %  urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=doc.name, ballot_id=ballot.pk))\n\n    my_blocking = False\n    for i, (balloter, pos) in enumerate(positions):\n        if user_is_person(user,balloter) and pos and pos.pos.blocking:\n            my_blocking = True\n            break\n\n    typename = \"Unknown\"\n    if ballot.ballot_type.slug=='irsg-approve':\n        typename = \"IRSG\"\n    else:\n        typename = \"IESG\"\n\n    res = ['<a %s href=\"%s\" data-bs-toggle=\"modal\" data-bs-target=\"#modal-%d\" aria-label=\"%s positions\" title=\"%s positions (click to show more)\" class=\"ballot-icon\"><table' % (\n            right_click_string,\n            urlreverse(\"ietf.doc.views_doc.ballot_popup\", kwargs=dict(name=doc.name, ballot_id=ballot.pk)),\n            ballot.pk,\n            typename,\n            typename,)]\n    if my_blocking:\n        res.append(' class=\"is-blocking\" ')\n    res.append('><tbody>')\n\n    res.append(\"<tr>\")\n\n    for i, (ad, pos) in enumerate(positions):\n        # The IRSG has many more members than the IESG, so make the table wider\n        if i > 0 and i % (5 if len(positions) <= 15 else 10) == 0:\n            res.append(\"</tr><tr>\")\n\n        c = \"position-%s\" % (pos.pos.slug if pos else \"norecord\")\n\n        if user_is_person(user, ad):\n            c += \" my\"\n\n        res.append('<td class=\"%s\"></td>' % c)\n\n    # add sufficient table calls to last row to avoid HTML validation warning\n    while (i + 1) % 5 != 0:\n        res.append('<td class=\"position-empty\"></td>')\n        i = i + 1\n\n    res.append(\"</tr></tbody></table></a>\")\n    res.append('<div id=\"modal-%d\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\"><div class=\"modal-dialog modal-dialog-scrollable modal-xl\"><div class=\"modal-content\"></div></div></div>' % ballot.pk)\n\n    return mark_safe(\"\".join(res))\n\n@register.filter\ndef ballotposition(doc, user):\n    if not showballoticon(doc) or not has_role(user, \"Area Director\"):\n        return None\n\n    ballot = doc.active_ballot()\n    if not ballot:\n        return None\n\n    changed_pos = doc.latest_event(BallotPositionDocEvent, type=\"changed_ballot_position\", balloter__user=user, ballot=ballot)\n    if changed_pos:\n        pos = changed_pos.pos\n    else:\n        pos = BallotPositionName.objects.get(slug=\"norecord\")\n    return pos\n\n\n@register.filter\ndef state_age_colored(doc):\n    if doc.type_id == \"draft\":\n        if not doc.get_state_slug() in [\"active\", \"rfc\"]:\n            # Don't show anything for expired/withdrawn/replaced drafts\n            return \"\"\n        iesg_state = doc.get_state_slug(\"draft-iesg\")\n        if not iesg_state:\n            return \"\"\n\n        if iesg_state in [\"dead\", \"watching\", \"pub\", \"idexists\"]:\n            return \"\"\n        try:\n            state_datetime = (\n                doc.docevent_set.filter(\n                    Q(type=\"started_iesg_process\")\n                    | Q(type=\"changed_state\", statedocevent__state_type=\"draft-iesg\")\n                )\n                .order_by(\"-time\")[0]\n                .time\n            )\n        except IndexError:\n            state_datetime = datetime.datetime(1990, 1, 1, tzinfo=datetime.timezone.utc)\n        days = (timezone.now() - state_datetime).days\n        # loosely based on\n        # https://trac.ietf.org/trac/iesg/wiki/PublishPath\n        if iesg_state == \"lc\":\n            goal1 = 30\n            goal2 = 30\n        elif iesg_state == \"rfcqueue\":\n            goal1 = 60\n            goal2 = 120\n        elif iesg_state in [\"lc-req\", \"ann\"]:\n            goal1 = 4\n            goal2 = 7\n        elif \"need-rev\" in [x.slug for x in doc.tags.all()]:\n            goal1 = 14\n            goal2 = 28\n        elif iesg_state == \"pub-req\":\n            goal1 = 7\n            goal2 = 14\n        elif iesg_state == \"ad-eval\":\n            goal1 = 14\n            goal2 = 28\n        else:\n            goal1 = 14\n            goal2 = 28\n        if days > goal2:\n            class_name = \"bg-danger\"\n        elif days > goal1:\n            class_name = \"bg-warning\"\n        else:\n            # don't show a badge when things are in the green; clutters display\n            # class_name = \"text-success\"\n            return \"\"\n        if days > goal1:\n            title = ' title=\"In state for %d day%s; goal is &lt;%d days.\"' % (\n                days,\n                \"s\" if days != 1 else \"\",\n                goal1,\n            )\n        else:\n            title = \"\"\n        return mark_safe(\n            '<span class=\"badge rounded-pill %s\" %s><i class=\"bi bi-clock-fill\"></i> %d</span>'\n            % (class_name, title, days)\n        )\n    else:\n        return \"\"\n\n\n@register.filter\ndef auth48_alert_badge(doc):\n    \"\"\"Return alert badge, if any, for a document\"\"\"\n    if doc.type_id != 'draft':\n        return ''\n\n    iesg_state = doc.get_state_slug('draft-iesg')\n    if iesg_state != 'rfcqueue':\n        return ''\n\n    rfced_state = doc.get_state_slug('draft-rfceditor')\n    if rfced_state == 'auth48':\n        return mark_safe('<span class=\"badge rounded-pill bg-info\" title=\"AUTH48\">AUTH48</span>')\n\n    return ''\n", "path": "ietf/doc/templatetags/ballot_icon.py"}], "after_files": [{"content": "# Copyright The IETF Trust 2012-2021, All Rights Reserved\n# Copyright (C) 2009-2010 Nokia Corporation and/or its subsidiary(-ies).\n# All rights reserved. Contact: Pasi Eronen <pasi.eronen@nokia.com>\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions\n# are met:\n#\n#  * Redistributions of source code must retain the above copyright\n#    notice, this list of conditions and the following disclaimer.\n#\n#  * Redistributions in binary form must reproduce the above\n#    copyright notice, this list of conditions and the following\n#    disclaimer in the documentation and/or other materials provided\n#    with the distribution.\n#\n#  * Neither the name of the Nokia Corporation and/or its\n#    subsidiary(-ies) nor the names of its contributors may be used\n#    to endorse or promote products derived from this software\n#    without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nimport datetime\n\nimport debug      # pyflakes:ignore\n\nfrom django import template\nfrom django.urls import reverse as urlreverse\nfrom django.db.models import Q\nfrom django.utils import timezone\nfrom django.utils.safestring import mark_safe\n\nfrom ietf.ietfauth.utils import user_is_person, has_role\nfrom ietf.doc.models import BallotPositionDocEvent, IESG_BALLOT_ACTIVE_STATES\nfrom ietf.name.models import BallotPositionName\n\n\nregister = template.Library()\n\n@register.filter\ndef showballoticon(doc):\n    if doc.type_id == \"draft\":\n        if doc.stream_id == 'ietf' and doc.get_state_slug(\"draft-iesg\") not in IESG_BALLOT_ACTIVE_STATES:\n            return False\n        elif doc.stream_id == 'irtf' and doc.get_state_slug(\"draft-stream-irtf\") not in ['irsgpoll']:\n            return False\n    elif doc.type_id == \"charter\":\n        if doc.get_state_slug() not in (\"intrev\", \"extrev\", \"iesgrev\"):\n            return False\n    elif doc.type_id == \"conflrev\":\n       if doc.get_state_slug() not in (\"iesgeval\",\"defer\"):\n           return False\n    elif doc.type_id == \"statchg\":\n       if doc.get_state_slug() not in (\"iesgeval\",\"defer\", \"in-lc\"):\n           return False\n\n    return True\n\n@register.simple_tag(takes_context=True)\ndef ballot_icon(context, doc):\n    user = context.get(\"user\")\n\n    if not doc:\n        return \"\"\n\n    if not showballoticon(doc):\n        return \"\"\n\n    ballot = doc.ballot if hasattr(doc, 'ballot') else doc.active_ballot()\n\n    if not ballot:\n        return \"\"\n\n    def sort_key(t):\n        _, pos = t\n        if not pos:\n            return (2, 0)\n        elif pos.pos.blocking:\n            return (0, pos.pos.order)\n        else:\n            return (1, pos.pos.order)\n\n    positions = list(ballot.active_balloter_positions().items())\n    positions.sort(key=sort_key)\n\n    right_click_string = ''\n    if has_role(user, \"Area Director\"):\n        right_click_string = 'oncontextmenu=\"window.location.href=\\'%s\\';return false;\"' %  urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=doc.name, ballot_id=ballot.pk))\n\n    my_blocking = False\n    for i, (balloter, pos) in enumerate(positions):\n        if user_is_person(user,balloter) and pos and pos.pos.blocking:\n            my_blocking = True\n            break\n\n    typename = \"Unknown\"\n    if ballot.ballot_type.slug=='irsg-approve':\n        typename = \"IRSG\"\n    else:\n        typename = \"IESG\"\n\n    res = ['<a %s href=\"%s\" data-bs-toggle=\"modal\" data-bs-target=\"#modal-%d\" aria-label=\"%s positions\" title=\"%s positions (click to show more)\" class=\"ballot-icon\"><table' % (\n            right_click_string,\n            urlreverse(\"ietf.doc.views_doc.ballot_popup\", kwargs=dict(name=doc.name, ballot_id=ballot.pk)),\n            ballot.pk,\n            typename,\n            typename,)]\n    if my_blocking:\n        res.append(' class=\"is-blocking\" ')\n    res.append('><tbody>')\n\n    res.append(\"<tr>\")\n\n    for i, (ad, pos) in enumerate(positions):\n        # The IRSG has many more members than the IESG, so make the table wider\n        if i > 0 and i % (5 if len(positions) <= 15 else 10) == 0:\n            res.append(\"</tr><tr>\")\n\n        c = \"position-%s\" % (pos.pos.slug if pos else \"norecord\")\n\n        if user_is_person(user, ad):\n            c += \" my\"\n\n        res.append('<td class=\"%s\"></td>' % c)\n\n    # add sufficient table calls to last row to avoid HTML validation warning\n    while (i + 1) % 5 != 0:\n        res.append('<td class=\"position-empty\"></td>')\n        i = i + 1\n\n    res.append(\"</tr></tbody></table></a>\")\n    res.append('<div id=\"modal-%d\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\"><div class=\"modal-dialog modal-dialog-scrollable modal-xl modal-fullscreen-lg-down\"><div class=\"modal-content\"></div></div></div>' % ballot.pk)\n\n    return mark_safe(\"\".join(res))\n\n@register.filter\ndef ballotposition(doc, user):\n    if not showballoticon(doc) or not has_role(user, \"Area Director\"):\n        return None\n\n    ballot = doc.active_ballot()\n    if not ballot:\n        return None\n\n    changed_pos = doc.latest_event(BallotPositionDocEvent, type=\"changed_ballot_position\", balloter__user=user, ballot=ballot)\n    if changed_pos:\n        pos = changed_pos.pos\n    else:\n        pos = BallotPositionName.objects.get(slug=\"norecord\")\n    return pos\n\n\n@register.filter\ndef state_age_colored(doc):\n    if doc.type_id == \"draft\":\n        if not doc.get_state_slug() in [\"active\", \"rfc\"]:\n            # Don't show anything for expired/withdrawn/replaced drafts\n            return \"\"\n        iesg_state = doc.get_state_slug(\"draft-iesg\")\n        if not iesg_state:\n            return \"\"\n\n        if iesg_state in [\"dead\", \"watching\", \"pub\", \"idexists\"]:\n            return \"\"\n        try:\n            state_datetime = (\n                doc.docevent_set.filter(\n                    Q(type=\"started_iesg_process\")\n                    | Q(type=\"changed_state\", statedocevent__state_type=\"draft-iesg\")\n                )\n                .order_by(\"-time\")[0]\n                .time\n            )\n        except IndexError:\n            state_datetime = datetime.datetime(1990, 1, 1, tzinfo=datetime.timezone.utc)\n        days = (timezone.now() - state_datetime).days\n        # loosely based on\n        # https://trac.ietf.org/trac/iesg/wiki/PublishPath\n        if iesg_state == \"lc\":\n            goal1 = 30\n            goal2 = 30\n        elif iesg_state == \"rfcqueue\":\n            goal1 = 60\n            goal2 = 120\n        elif iesg_state in [\"lc-req\", \"ann\"]:\n            goal1 = 4\n            goal2 = 7\n        elif \"need-rev\" in [x.slug for x in doc.tags.all()]:\n            goal1 = 14\n            goal2 = 28\n        elif iesg_state == \"pub-req\":\n            goal1 = 7\n            goal2 = 14\n        elif iesg_state == \"ad-eval\":\n            goal1 = 14\n            goal2 = 28\n        else:\n            goal1 = 14\n            goal2 = 28\n        if days > goal2:\n            class_name = \"bg-danger\"\n        elif days > goal1:\n            class_name = \"bg-warning\"\n        else:\n            # don't show a badge when things are in the green; clutters display\n            # class_name = \"text-success\"\n            return \"\"\n        if days > goal1:\n            title = ' title=\"In state for %d day%s; goal is &lt;%d days.\"' % (\n                days,\n                \"s\" if days != 1 else \"\",\n                goal1,\n            )\n        else:\n            title = \"\"\n        return mark_safe(\n            '<span class=\"badge rounded-pill %s\" %s><i class=\"bi bi-clock-fill\"></i> %d</span>'\n            % (class_name, title, days)\n        )\n    else:\n        return \"\"\n\n\n@register.filter\ndef auth48_alert_badge(doc):\n    \"\"\"Return alert badge, if any, for a document\"\"\"\n    if doc.type_id != 'draft':\n        return ''\n\n    iesg_state = doc.get_state_slug('draft-iesg')\n    if iesg_state != 'rfcqueue':\n        return ''\n\n    rfced_state = doc.get_state_slug('draft-rfceditor')\n    if rfced_state == 'auth48':\n        return mark_safe('<span class=\"badge rounded-pill bg-info\" title=\"AUTH48\">AUTH48</span>')\n\n    return ''\n", "path": "ietf/doc/templatetags/ballot_icon.py"}]}