Repository: wagtail/wagtail

Issue:
Please add a progress bar to "wagtail_update_image_renditions" management command
I love this new management command to regenerate all image renditions `wagtail_update_image_renditions`. But often the websites I had to use it on had many thousands of images and this command would need some hours to complete. I would love to see some kind of **progress feedback** in the terminal to get an idea of the time this task might need to complete. This could be text based of even a bar, like we know from packages like "tqdm".


Discussion:
I think this enhancement is better, i have ran it locally:
without status bar:
![without_bar](https://github.com/wagtail/wagtail/assets/68147015/4e0742fb-4b55-4ac2-bb31-abf8c00fd99e)

with status bar:
![with_status_bar](https://github.com/wagtail/wagtail/assets/68147015/88a793a3-c9b6-40c4-a9e1-f72d63f3617c)

but before creating pr i would like to discuss it in community channel since its very minor change


Nice! I would vote for the status bar!

Resolving patch:
diff --git a/wagtail/images/management/commands/wagtail_update_image_renditions.py b/wagtail/images/management/commands/wagtail_update_image_renditions.py
--- a/wagtail/images/management/commands/wagtail_update_image_renditions.py
+++ b/wagtail/images/management/commands/wagtail_update_image_renditions.py
@@ -8,6 +8,17 @@
 logger = logging.getLogger(__name__)
 
 
+def progress_bar(current, total, bar_length=50):
+    fraction = current / total
+
+    arrow = int(fraction * bar_length - 1) * "-" + ">"
+    padding = int(bar_length - len(arrow)) * " "
+
+    ending = "\n" if current == total else "\r"
+
+    return (f"Progress: [{arrow}{padding}] {int(fraction*100)}%", ending)
+
+
 class Command(BaseCommand):
     """Command to create missing image renditions with the option to remove (purge) any existing ones."""
 
@@ -49,6 +60,7 @@ def handle(self, *args, **options):
                 self.style.HTTP_INFO(f"Regenerating {num_renditions} rendition(s)")
             )
 
+        progress_bar_current = 1
         for rendition in (
             # Pre-calculate the ids of the renditions to change,
             # otherwise `.iterator` never ends.
@@ -64,6 +76,10 @@ def handle(self, *args, **options):
                     # Delete the existing rendition
                     rendition.delete()
 
+                    _progress_bar = progress_bar(progress_bar_current, num_renditions)
+                    self.stdout.write(_progress_bar[0], ending=_progress_bar[1])
+                    progress_bar_current = progress_bar_current + 1
+
                     if not purge_only:
                         # Create a new one
                         rendition_image.get_rendition(rendition_filter)


Test patch:
diff --git a/wagtail/images/tests/test_management_commands.py b/wagtail/images/tests/test_management_commands.py
--- a/wagtail/images/tests/test_management_commands.py
+++ b/wagtail/images/tests/test_management_commands.py
@@ -5,6 +5,7 @@
 from django.core import management
 from django.test import TestCase, override_settings
 
+from ..management.commands.wagtail_update_image_renditions import progress_bar
 from .utils import Image, get_test_image_file
 
 # note .utils.Image already does get_image_model()
@@ -49,6 +50,29 @@ def run_command(self, **options):
 
         return output
 
+    def test_progress_bar(self):
+        total_rendition = 10
+        out = StringIO()
+        for current in range(1, total_rendition + 1):
+            progress_bar_output = progress_bar(current, total_rendition)[0]
+            out.write(progress_bar_output)
+        out.seek(0)
+        expected_output = "".join(
+            [
+                "Progress: [---->                                             ] 10%",
+                "Progress: [--------->                                        ] 20%",
+                "Progress: [-------------->                                   ] 30%",
+                "Progress: [------------------->                              ] 40%",
+                "Progress: [------------------------>                         ] 50%",
+                "Progress: [----------------------------->                    ] 60%",
+                "Progress: [---------------------------------->               ] 70%",
+                "Progress: [--------------------------------------->          ] 80%",
+                "Progress: [-------------------------------------------->     ] 90%",
+                "Progress: [------------------------------------------------->] 100%",
+            ]
+        )
+        self.assertIn(expected_output, out.getvalue())
+
     def test_exits_early_for_no_renditions(self):
         self.delete_renditions()
         # checking when command is called without any arguments
@@ -70,6 +94,7 @@ def test_image_renditions(self):
         self.assertEqual(
             output_string,
             f"Regenerating {total_renditions} rendition(s)\n"
+            f"Progress: [------------------------------------------------->] 100%\n"
             f"Successfully processed {total_renditions} rendition(s)\n",
         )
 
@@ -87,6 +112,7 @@ def test_image_renditions_with_purge_only(self):
         self.assertEqual(
             output_string,
             f"Purging {total_renditions} rendition(s)\n"
+            f"Progress: [------------------------------------------------->] 100%\n"
             f"Successfully processed {total_renditions} rendition(s)\n",
         )