From bf19d3d6b8602316a74355895a4e705bd32e5239 Mon Sep 17 00:00:00 2001
From: Kegan Maher <kegan@compiler.la>
Date: Wed, 30 Oct 2024 16:42:13 +0000
Subject: [PATCH] feat(cli): define the agency list command

    benefits agency list -h
---
 benefits/cli/agency/__init__.py               |  0
 benefits/cli/agency/list.py                   | 73 +++++++++++++++++++
 benefits/cli/management/commands/agency.py    |  6 +-
 tests/pytest/cli/agency/__init__.py           |  0
 tests/pytest/cli/agency/test_list.py          | 68 +++++++++++++++++
 tests/pytest/cli/conftest.py                  | 12 +++
 .../cli/management/commands/test_agency.py    |  7 +-
 7 files changed, 163 insertions(+), 3 deletions(-)
 create mode 100644 benefits/cli/agency/__init__.py
 create mode 100644 benefits/cli/agency/list.py
 create mode 100644 tests/pytest/cli/agency/__init__.py
 create mode 100644 tests/pytest/cli/agency/test_list.py
 create mode 100644 tests/pytest/cli/conftest.py

diff --git a/benefits/cli/agency/__init__.py b/benefits/cli/agency/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/benefits/cli/agency/list.py b/benefits/cli/agency/list.py
new file mode 100644
index 0000000000..974b84c2e6
--- /dev/null
+++ b/benefits/cli/agency/list.py
@@ -0,0 +1,73 @@
+from dataclasses import dataclass
+
+from django.db.models import Q
+
+from benefits.cli.commands import BaseOptions, BenefitsCommand
+from benefits.core.models import TransitAgency
+
+
+@dataclass
+class Options(BaseOptions):
+    all: bool = False
+    name: str = None
+    slug: str = None
+
+
+class List(BenefitsCommand):
+    """List transit agencies."""
+
+    help = __doc__
+    name = "list"
+    options_cls = Options
+
+    def add_arguments(self, parser):
+        parser.add_argument(
+            "-a",
+            "--all",
+            action="store_true",
+            default=False,
+            help="Show both active and inactive agencies. By default show only active agencies.",
+        )
+        parser.add_argument(
+            "-n",
+            "--name",
+            type=str,
+            help="Filter for agencies with matching (partial) short_name or long_name.",
+        )
+        parser.add_argument(
+            "-s",
+            "--slug",
+            type=str,
+            help="Filter for agencies with matching (partial) slug.",
+        )
+
+    def handle(self, *args, **options):
+        opts = self.parse_opts(**options)
+        agencies = TransitAgency.objects.all()
+
+        if not opts.all:
+            agencies = agencies.filter(active=True)
+
+        if opts.name:
+            q = Q(short_name__contains=opts.name) | Q(long_name__contains=opts.name)
+            agencies = agencies.filter(q)
+
+        if opts.slug:
+            agencies = agencies.filter(slug__contains=opts.slug)
+
+        if len(agencies) > 0:
+            if len(agencies) > 1:
+                msg = f"{len(agencies)} agencies:"
+            else:
+                msg = "1 agency:"
+            self.stdout.write(self.style.SUCCESS(msg))
+
+            active = filter(lambda a: a.active, agencies)
+            inactive = filter(lambda a: not a.active, agencies)
+
+            for agency in active:
+                self.stdout.write(self.style.HTTP_NOT_MODIFIED(f"{agency}"))
+            for agency in inactive:
+                self.stdout.write(self.style.WARNING(f"[inactive] {agency}"))
+        else:
+            self.stdout.write(self.style.HTTP_NOT_FOUND("No matching agencies"))
diff --git a/benefits/cli/management/commands/agency.py b/benefits/cli/management/commands/agency.py
index 9c24a9f18e..25196c5401 100644
--- a/benefits/cli/management/commands/agency.py
+++ b/benefits/cli/management/commands/agency.py
@@ -1,3 +1,4 @@
+from benefits.cli.agency.list import List
 from benefits.cli.commands import BenefitsCommand
 
 
@@ -6,7 +7,8 @@ class Command(BenefitsCommand):
 
     help = __doc__
     name = "agency"
-    subcommands = []
+    subcommands = [List]
 
     def __init__(self, stdout=None, stderr=None, no_color=False, force_color=False):
-        super().__init__(stdout, stderr, no_color, force_color)
+        # make List the default_subcmd
+        super().__init__(stdout, stderr, no_color, force_color, List)
diff --git a/tests/pytest/cli/agency/__init__.py b/tests/pytest/cli/agency/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/pytest/cli/agency/test_list.py b/tests/pytest/cli/agency/test_list.py
new file mode 100644
index 0000000000..34d3ddab0f
--- /dev/null
+++ b/tests/pytest/cli/agency/test_list.py
@@ -0,0 +1,68 @@
+import pytest
+
+from benefits.cli.agency.list import List
+
+
+@pytest.fixture
+def cmd(cmd):
+    def call(*args, **kwargs):
+        return cmd(List, *args, **kwargs)
+
+    return call
+
+
+@pytest.mark.django_db
+def test_list(cmd):
+    out, err = cmd()
+
+    assert err == ""
+    assert "No matching agencies" in out
+
+
+@pytest.mark.django_db
+def test_list_agency(cmd, model_TransitAgency):
+    out, err = cmd()
+
+    assert err == ""
+    assert "1 agency" in out
+    assert str(model_TransitAgency) in out
+
+
+@pytest.mark.django_db
+def test_list_agencies(cmd, model_TransitAgency):
+    orig_agency = str(model_TransitAgency)
+
+    model_TransitAgency.pk = None
+    model_TransitAgency.long_name = "Another agency"
+    model_TransitAgency.save()
+
+    out, err = cmd()
+
+    assert err == ""
+    assert "2 agencies" in out
+    assert orig_agency in out
+    assert str(model_TransitAgency) in out
+
+
+@pytest.mark.django_db
+def test_list_agencies_active(cmd, model_TransitAgency):
+    orig_agency = str(model_TransitAgency)
+
+    model_TransitAgency.pk = None
+    model_TransitAgency.long_name = "Another agency"
+    model_TransitAgency.active = False
+    model_TransitAgency.save()
+
+    out, err = cmd()
+
+    assert err == ""
+    assert "1 agency" in out
+    assert orig_agency in out
+    assert str(model_TransitAgency) not in out
+
+    out, err = cmd("--all")
+
+    assert err == ""
+    assert "2 agencies" in out
+    assert orig_agency in out
+    assert f"[inactive] {model_TransitAgency}" in out
diff --git a/tests/pytest/cli/conftest.py b/tests/pytest/cli/conftest.py
new file mode 100644
index 0000000000..1f7e3b5c4f
--- /dev/null
+++ b/tests/pytest/cli/conftest.py
@@ -0,0 +1,12 @@
+import pytest
+
+from django.core.management import call_command
+
+
+@pytest.fixture
+def cmd(capfd):
+    def call(cls, *args, **kwargs):
+        call_command(cls(), *args, **kwargs)
+        return capfd.readouterr()
+
+    return call
diff --git a/tests/pytest/cli/management/commands/test_agency.py b/tests/pytest/cli/management/commands/test_agency.py
index ba795df6f6..085d7dd5dd 100644
--- a/tests/pytest/cli/management/commands/test_agency.py
+++ b/tests/pytest/cli/management/commands/test_agency.py
@@ -1,5 +1,6 @@
 import pytest
 
+from benefits.cli.agency.list import List
 from benefits.cli.management.commands.agency import Command
 
 
@@ -7,7 +8,7 @@
 def test_class():
     assert Command.help == Command.__doc__
     assert Command.name == "agency"
-    assert Command.subcommands == []
+    assert Command.subcommands == [List]
 
 
 @pytest.mark.django_db
@@ -16,3 +17,7 @@ def test_init():
 
     assert "agency" in cmd.subparsers
     assert cmd.subparser == cmd.subparsers["agency"]
+
+    list_cmd = getattr(cmd, "list")
+    assert isinstance(list_cmd, List)
+    assert cmd.default_handler == list_cmd.handle