aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/cogs/moderation/management.py8
-rw-r--r--bot/pagination.py66
-rw-r--r--tests/bot/test_pagination.py41
3 files changed, 98 insertions, 17 deletions
diff --git a/bot/cogs/moderation/management.py b/bot/cogs/moderation/management.py
index 250a24247..ad17a90b0 100644
--- a/bot/cogs/moderation/management.py
+++ b/bot/cogs/moderation/management.py
@@ -83,14 +83,14 @@ class ModManagement(commands.Cog):
"actor__id": ctx.author.id,
"ordering": "-inserted_at"
}
- infractions = await self.bot.api_client.get(f"bot/infractions", params=params)
+ infractions = await self.bot.api_client.get("bot/infractions", params=params)
if infractions:
old_infraction = infractions[0]
infraction_id = old_infraction["id"]
else:
await ctx.send(
- f":x: Couldn't find most recent infraction; you have never given an infraction."
+ ":x: Couldn't find most recent infraction; you have never given an infraction."
)
return
else:
@@ -224,7 +224,7 @@ class ModManagement(commands.Cog):
) -> None:
"""Send a paginated embed of infractions for the specified user."""
if not infractions:
- await ctx.send(f":warning: No infractions could be found for that query.")
+ await ctx.send(":warning: No infractions could be found for that query.")
return
lines = tuple(
@@ -268,12 +268,12 @@ class ModManagement(commands.Cog):
User: {self.bot.get_user(user_id)} (`{user_id}`)
Type: **{infraction["type"]}**
Shadow: {hidden}
- Reason: {infraction["reason"] or "*None*"}
Created: {created}
Expires: {expires}
Remaining: {remaining}
Actor: {actor.mention if actor else actor_id}
ID: `{infraction["id"]}`
+ Reason: {infraction["reason"] or "*None*"}
{"**===============**" if active else "==============="}
""")
diff --git a/bot/pagination.py b/bot/pagination.py
index 90c8f849c..5c7be564d 100644
--- a/bot/pagination.py
+++ b/bot/pagination.py
@@ -37,12 +37,19 @@ class LinePaginator(Paginator):
The suffix appended at the end of every page. e.g. three backticks.
* max_size: `int`
The maximum amount of codepoints allowed in a page.
+ * scale_to_size: `int`
+ The maximum amount of characters a single line can scale up to.
* max_lines: `int`
The maximum amount of lines allowed in a page.
"""
def __init__(
- self, prefix: str = '```', suffix: str = '```', max_size: int = 2000, max_lines: int = None
+ self,
+ prefix: str = '```',
+ suffix: str = '```',
+ max_size: int = 2000,
+ scale_to_size: int = 2000,
+ max_lines: t.Optional[int] = None
) -> None:
"""
This function overrides the Paginator.__init__ from inside discord.ext.commands.
@@ -52,6 +59,10 @@ class LinePaginator(Paginator):
self.prefix = prefix
self.suffix = suffix
self.max_size = max_size - len(suffix)
+ if scale_to_size < max_size:
+ raise ValueError("scale_to_size must be >= max_size.")
+
+ self.scale_to_size = scale_to_size
self.max_lines = max_lines
self._current_page = [prefix]
self._linecount = 0
@@ -62,14 +73,26 @@ class LinePaginator(Paginator):
"""
Adds a line to the current page.
- If the line exceeds the `self.max_size` then an exception is raised.
+ If the line exceeds `self.max_size`, then `self.max_size` will go up to `scale_to_size` for
+ a single line before creating a new page. If it is still exceeded, the excess characters
+ are stored and placed on the next pages until there are none remaining (by word boundary).
+
+ Raises a RuntimeError if `self.max_size` is still exceeded after attempting to continue
+ onto the next page.
This function overrides the `Paginator.add_line` from inside `discord.ext.commands`.
It overrides in order to allow us to configure the maximum number of lines per page.
"""
- if len(line) > self.max_size - len(self.prefix) - 2:
- raise RuntimeError('Line exceeds maximum page size %s' % (self.max_size - len(self.prefix) - 2))
+ remaining_words = None
+ if len(line) > (max_chars := self.max_size - len(self.prefix) - 2):
+ if len(line) > self.scale_to_size:
+ line, remaining_words = self._split_remaining_words(line, max_chars)
+ # If line still exceeds scale_to_size, we were unable to split into a second
+ # page without truncating.
+ if len(line) > self.scale_to_size:
+ raise RuntimeError(f'Line exceeds maximum scale_to_size {self.scale_to_size}'
+ ' and could not be split.')
if self.max_lines is not None:
if self._linecount >= self.max_lines:
@@ -87,6 +110,36 @@ class LinePaginator(Paginator):
self._current_page.append('')
self._count += 1
+ if remaining_words:
+ self.add_line(remaining_words)
+
+ def _split_remaining_words(self, line: str, max_chars: int) -> t.Tuple[str, t.Optional[str]]:
+ """Internal: split a line into two strings; one that fits within *max_chars* characters
+ (reduced_words) and another for the remaining (remaining_words), rounding down to the
+ nearest word.
+
+ Return a tuple in the format (reduced_words, remaining_words).
+ """
+ reduced_words = []
+ # "(Continued)" is used on a line by itself to indicate the continuation of last page
+ remaining_words = ["(Continued)\n", "---------------\n"]
+ reduced_char_count = 0
+ is_full = False
+
+ for word in line.split(" "):
+ if not is_full:
+ if len(word) + reduced_char_count <= max_chars:
+ reduced_words.append(word)
+ reduced_char_count += len(word)
+ else:
+ is_full = True
+ remaining_words.append(word)
+ else:
+ remaining_words.append(word)
+
+ return " ".join(reduced_words), " ".join(remaining_words) if len(remaining_words) > 2 \
+ else None
+
@classmethod
async def paginate(
cls,
@@ -97,6 +150,7 @@ class LinePaginator(Paginator):
suffix: str = "",
max_lines: t.Optional[int] = None,
max_size: int = 500,
+ scale_to_size: int = 2000,
empty: bool = True,
restrict_to_user: User = None,
timeout: int = 300,
@@ -147,7 +201,7 @@ class LinePaginator(Paginator):
if not lines:
if exception_on_empty_embed:
- log.exception(f"Pagination asked for empty lines iterable")
+ log.exception("Pagination asked for empty lines iterable")
raise EmptyPaginatorEmbed("No lines to paginate")
log.debug("No lines to add to paginator, adding '(nothing to display)' message")
@@ -357,7 +411,7 @@ class ImagePaginator(Paginator):
if not pages:
if exception_on_empty_embed:
- log.exception(f"Pagination asked for empty image list")
+ log.exception("Pagination asked for empty image list")
raise EmptyPaginatorEmbed("No images to paginate")
log.debug("No images to add to paginator, adding '(no images to display)' message")
diff --git a/tests/bot/test_pagination.py b/tests/bot/test_pagination.py
index 0a734b505..f2e2c27ce 100644
--- a/tests/bot/test_pagination.py
+++ b/tests/bot/test_pagination.py
@@ -8,17 +8,44 @@ class LinePaginatorTests(TestCase):
def setUp(self):
"""Create a paginator for the test method."""
- self.paginator = pagination.LinePaginator(prefix='', suffix='', max_size=30)
-
- def test_add_line_raises_on_too_long_lines(self):
- """`add_line` should raise a `RuntimeError` for too long lines."""
- message = f"Line exceeds maximum page size {self.paginator.max_size - 2}"
- with self.assertRaises(RuntimeError, msg=message):
- self.paginator.add_line('x' * self.paginator.max_size)
+ self.paginator = pagination.LinePaginator(prefix='', suffix='', max_size=30,
+ scale_to_size=50)
def test_add_line_works_on_small_lines(self):
"""`add_line` should allow small lines to be added."""
self.paginator.add_line('x' * (self.paginator.max_size - 3))
+ # Note that the page isn't added to _pages until it's full.
+ self.assertEqual(len(self.paginator._pages), 0)
+
+ def test_add_line_works_on_long_lines(self):
+ """`add_line` should scale long lines up to `scale_to_size`."""
+ self.paginator.add_line('x' * self.paginator.scale_to_size)
+ self.assertEqual(len(self.paginator._pages), 1)
+
+ # Any additional lines should start a new page after `max_size` is exceeded.
+ self.paginator.add_line('x')
+ self.assertEqual(len(self.paginator._pages), 2)
+
+ def test_add_line_continuation(self):
+ """When `scale_to_size` is exceeded, remaining words should be split onto the next page."""
+ self.paginator.add_line('zyz ' * (self.paginator.scale_to_size//4 + 1))
+ self.assertEqual(len(self.paginator._pages), 2)
+
+ def test_add_line_no_continuation(self):
+ """If adding a new line to an existing page would exceed `max_size`, it should start a new
+ page rather than using continuation.
+ """
+ self.paginator.add_line('z' * (self.paginator.max_size - 3))
+ self.paginator.add_line('z')
+ self.assertEqual(len(self.paginator._pages), 1)
+
+ def test_add_line_raises_on_very_long_words(self):
+ """`add_line` should raise if a single long word is added that exceeds `scale_to_size`.
+
+ Note: truncation is also a potential option, but this should not occur from normal usage.
+ """
+ with self.assertRaises(RuntimeError):
+ self.paginator.add_line('x' * (self.paginator.scale_to_size + 1))
class ImagePaginatorTests(TestCase):