Skip to content

Commit 957cd5c

Browse files
committed
feat(cli): define the agency create command
benefits agency create -h
1 parent ee0097e commit 957cd5c

File tree

6 files changed

+207
-2
lines changed

6 files changed

+207
-2
lines changed

benefits/cli/agency/create.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
from dataclasses import dataclass
2+
from pathlib import Path
3+
4+
from django.core.management.base import CommandError
5+
6+
from benefits.cli.commands import BaseOptions, BenefitsCommand
7+
from benefits.core.models import TransitAgency, TransitProcessor
8+
9+
10+
@dataclass
11+
class Options(BaseOptions):
12+
active: bool = False
13+
info_url: str = None
14+
long_name: str = None
15+
phone: str = None
16+
short_name: str = None
17+
slug: str = None
18+
templates: bool = False
19+
templates_only: bool = False
20+
transit_processor: int = None
21+
22+
def __post_init__(self):
23+
if not self.short_name:
24+
self.short_name = self.slug.upper()
25+
if not self.long_name:
26+
self.long_name = self.slug.upper()
27+
28+
29+
class Create(BenefitsCommand):
30+
"""Create a new transit agency."""
31+
32+
help = __doc__
33+
name = "create"
34+
options_cls = Options
35+
sample_slug = "cst"
36+
templates = [
37+
f"core/index--{sample_slug}.html",
38+
f"eligibility/index--{sample_slug}.html",
39+
]
40+
41+
@property
42+
def template_paths(self):
43+
return [self.template_path(t) for t in self.templates]
44+
45+
def _create_agency(self, opts: Options) -> TransitAgency:
46+
if isinstance(opts.transit_processor, int):
47+
transit_processor = TransitProcessor.objects.get(id=opts.transit_processor)
48+
else:
49+
transit_processor = None
50+
51+
agency = TransitAgency.objects.create(
52+
active=opts.active,
53+
slug=opts.slug,
54+
info_url=opts.info_url,
55+
long_name=opts.long_name,
56+
phone=opts.phone,
57+
short_name=opts.short_name,
58+
transit_processor=transit_processor,
59+
)
60+
agency.save()
61+
62+
return agency
63+
64+
def _create_templates(self, agency: TransitAgency):
65+
for template in self.template_paths:
66+
content = template.read_text().replace(self.sample_slug, agency.slug)
67+
content = content.replace(self.sample_slug.upper(), agency.slug.upper())
68+
69+
path = str(template.resolve()).replace(self.sample_slug, agency.slug)
70+
71+
new_template = Path(path)
72+
new_template.write_text(content)
73+
74+
def _raise_for_slug(self, opts: Options) -> bool:
75+
if TransitAgency.by_slug(opts.slug):
76+
raise CommandError(f"TransitAgency with slug already exists: {opts.slug}")
77+
78+
def add_arguments(self, parser):
79+
parser.add_argument("-a", "--active", action="store_true", default=False, help="Activate the agency")
80+
parser.add_argument(
81+
"-i", "--info-url", type=str, default="https://agency.com", help="The agency's informational website URL"
82+
)
83+
parser.add_argument("-l", "--long-name", type=str, default="Regional Transit Agency", help="The agency's long name")
84+
parser.add_argument("-p", "--phone", type=str, default="800-555-5555", help="The agency's phone number")
85+
parser.add_argument("-s", "--short-name", type=str, default="Agency", help="The agency's short name")
86+
parser.add_argument("--templates", action="store_true", default=False, help="Also create templates for the agency")
87+
parser.add_argument(
88+
"--templates-only",
89+
action="store_true",
90+
default=False,
91+
help="Don't create the agency in the database, but scaffold templates",
92+
)
93+
parser.add_argument(
94+
"--transit-processor",
95+
type=int,
96+
choices=[t.id for t in TransitProcessor.objects.all()],
97+
default=TransitProcessor.objects.first().id,
98+
help="The id of a TransitProcessor instance to link to this agency",
99+
)
100+
parser.add_argument("slug", help="The agency's slug", type=str)
101+
102+
def handle(self, *args, **options):
103+
opts = self.parse_opts(**options)
104+
self._raise_for_slug(opts)
105+
106+
if not opts.templates_only:
107+
self.stdout.write(self.style.NOTICE("Creating new agency..."))
108+
agency = self._create_agency(opts)
109+
self.stdout.write(self.style.SUCCESS(f"Agency created: {agency.slug} (id={agency.id})"))
110+
111+
if opts.templates:
112+
self.stdout.write(self.style.NOTICE("Creating new agency templates..."))
113+
self._create_templates(agency)
114+
self.stdout.write(self.style.SUCCESS("Templates created"))

benefits/cli/commands.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from dataclasses import dataclass
2+
from pathlib import Path
23
from typing import Callable
34

5+
from django import template
46
from django.core.management.base import BaseCommand
57

68
from benefits import VERSION
@@ -108,3 +110,18 @@ def parse_opts(self, **options):
108110
"""Parse options into a dataclass instance."""
109111
options = {k: v for k, v in options.items() if k in dir(self.options_cls)}
110112
return self.options_cls(**options)
113+
114+
def template_path(self, template_name: str) -> Path:
115+
"""Get a `pathlib.Path` for the named template.
116+
117+
A `template_name` is the app-local name, e.g. `enrollment/success.html`.
118+
119+
Adapted from https://stackoverflow.com/a/75863472.
120+
"""
121+
for engine in template.engines.all():
122+
for loader in engine.engine.template_loaders:
123+
for origin in loader.get_template_sources(template_name):
124+
path = Path(origin.name)
125+
if path.exists():
126+
return path
127+
raise template.TemplateDoesNotExist(f"Could not find template: {template_name}")

benefits/cli/management/commands/agency.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from benefits.cli.agency.create import Create
12
from benefits.cli.agency.list import List
23
from benefits.cli.commands import BenefitsCommand
34

@@ -7,7 +8,7 @@ class Command(BenefitsCommand):
78

89
help = __doc__
910
name = "agency"
10-
subcommands = [List]
11+
subcommands = [List, Create]
1112

1213
def __init__(self, stdout=None, stderr=None, no_color=False, force_color=False):
1314
# make List the default_subcmd
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import pytest
2+
3+
from django.core.management.base import CommandError
4+
5+
from benefits.cli.agency.create import Create
6+
from benefits.core.models import TransitAgency
7+
8+
9+
@pytest.fixture
10+
def cmd(cmd):
11+
def call(*args, **kwargs):
12+
return cmd(Create, *args, **kwargs)
13+
14+
return call
15+
16+
17+
@pytest.mark.django_db
18+
def test_call_no_slug(cmd):
19+
with pytest.raises(CommandError, match="the following arguments are required: slug"):
20+
cmd()
21+
22+
23+
@pytest.mark.django_db
24+
def test_call(cmd, model_TransitProcessor):
25+
slug = "the-slug"
26+
27+
agency = TransitAgency.by_slug(slug)
28+
assert agency is None
29+
30+
out, err = cmd(slug)
31+
32+
assert err == ""
33+
assert "Creating new agency" in out
34+
assert f"Agency created: {slug}" in out
35+
36+
agency = TransitAgency.by_slug(slug)
37+
assert isinstance(agency, TransitAgency)
38+
assert agency.transit_processor == model_TransitProcessor
39+
40+
41+
@pytest.mark.django_db
42+
def test_call_dupe(cmd):
43+
slug = "the-slug"
44+
45+
# first time is OK
46+
cmd(slug)
47+
# again with the same slug, not OK
48+
with pytest.raises(CommandError, match=f"TransitAgency with slug already exists: {slug}"):
49+
cmd(slug)

tests/pytest/cli/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,8 @@ def call(cls, *args, **kwargs):
1010
return capfd.readouterr()
1111

1212
return call
13+
14+
15+
@pytest.fixture(autouse=True)
16+
def db_setup(model_TransitProcessor):
17+
pass

tests/pytest/cli/management/commands/test_agency.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
11
import pytest
22

3+
from benefits.cli.agency.create import Create
34
from benefits.cli.agency.list import List
45
from benefits.cli.management.commands.agency import Command
56

67

8+
@pytest.fixture
9+
def cmd(cmd):
10+
def call(*args, **kwargs):
11+
return cmd(Command, *args, **kwargs)
12+
13+
return call
14+
15+
716
@pytest.mark.django_db
817
def test_class():
918
assert Command.help == Command.__doc__
1019
assert Command.name == "agency"
11-
assert Command.subcommands == [List]
20+
21+
assert List in Command.subcommands
22+
assert Create in Command.subcommands
1223

1324

1425
@pytest.mark.django_db
@@ -21,3 +32,11 @@ def test_init():
2132
list_cmd = getattr(cmd, "list")
2233
assert isinstance(list_cmd, List)
2334
assert cmd.default_handler == list_cmd.handle
35+
36+
37+
@pytest.mark.django_db
38+
def test_call(cmd):
39+
out, err = cmd()
40+
41+
assert "No matching agencies" in out
42+
assert err == ""

0 commit comments

Comments
 (0)