Skip to content

Commit

Permalink
feat(cli): define the agency create command
Browse files Browse the repository at this point in the history
        benefits agency create -h
  • Loading branch information
thekaveman committed Dec 4, 2024
1 parent 2df1b41 commit 518fd02
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 2 deletions.
114 changes: 114 additions & 0 deletions benefits/cli/agency/create.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from dataclasses import dataclass
from pathlib import Path

from django.core.management.base import CommandError

from benefits.cli.commands import BaseOptions, BenefitsCommand
from benefits.core.models import TransitAgency, TransitProcessor


@dataclass
class Options(BaseOptions):
active: bool = False
info_url: str = None
long_name: str = None
phone: str = None
short_name: str = None
slug: str = None
templates: bool = False
templates_only: bool = False
transit_processor: int = None

def __post_init__(self):
if not self.short_name:
self.short_name = self.slug.upper()
if not self.long_name:
self.long_name = self.slug.upper()


class Create(BenefitsCommand):
"""Create a new transit agency."""

help = __doc__
name = "create"
options_cls = Options
sample_slug = "cst"
templates = [
f"core/index--{sample_slug}.html",
f"eligibility/index--{sample_slug}.html",
]

@property
def template_paths(self):
return [self.template_path(t) for t in self.templates]

def _create_agency(self, opts: Options) -> TransitAgency:
if isinstance(opts.transit_processor, int):
transit_processor = TransitProcessor.objects.get(id=opts.transit_processor)
else:
transit_processor = None

agency = TransitAgency.objects.create(
active=opts.active,
slug=opts.slug,
info_url=opts.info_url,
long_name=opts.long_name,
phone=opts.phone,
short_name=opts.short_name,
transit_processor=transit_processor,
)
agency.save()

return agency

def _create_templates(self, agency: TransitAgency):
for template in self.template_paths:
content = template.read_text().replace(self.sample_slug, agency.slug)
content = content.replace(self.sample_slug.upper(), agency.slug.upper())

path = str(template.resolve()).replace(self.sample_slug, agency.slug)

new_template = Path(path)
new_template.write_text(content)

def _raise_for_slug(self, opts: Options) -> bool:
if TransitAgency.by_slug(opts.slug):
raise CommandError(f"TransitAgency with slug already exists: {opts.slug}")

def add_arguments(self, parser):
parser.add_argument("-a", "--active", action="store_true", default=False, help="Activate the agency")
parser.add_argument(
"-i", "--info-url", type=str, default="https://agency.com", help="The agency's informational website URL"
)
parser.add_argument("-l", "--long-name", type=str, default="Regional Transit Agency", help="The agency's long name")
parser.add_argument("-p", "--phone", type=str, default="800-555-5555", help="The agency's phone number")
parser.add_argument("-s", "--short-name", type=str, default="Agency", help="The agency's short name")
parser.add_argument("--templates", action="store_true", default=False, help="Also create templates for the agency")
parser.add_argument(
"--templates-only",
action="store_true",
default=False,
help="Don't create the agency in the database, but scaffold templates",
)
parser.add_argument(
"--transit-processor",
type=int,
choices=[t.id for t in TransitProcessor.objects.all()],
default=TransitProcessor.objects.first().id,
help="The id of a TransitProcessor instance to link to this agency",
)
parser.add_argument("slug", help="The agency's slug", type=str)

def handle(self, *args, **options):
opts = self.parse_opts(**options)
self._raise_for_slug(opts)

if not opts.templates_only:
self.stdout.write(self.style.NOTICE("Creating new agency..."))
agency = self._create_agency(opts)
self.stdout.write(self.style.SUCCESS(f"Agency created: {agency.slug} (id={agency.id})"))

if opts.templates:
self.stdout.write(self.style.NOTICE("Creating new agency templates..."))
self._create_templates(agency)
self.stdout.write(self.style.SUCCESS("Templates created"))
17 changes: 17 additions & 0 deletions benefits/cli/commands.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from dataclasses import dataclass
from pathlib import Path
from typing import Callable

from django import template
from django.core.management.base import BaseCommand

from benefits import VERSION
Expand Down Expand Up @@ -108,3 +110,18 @@ def parse_opts(self, **options):
"""Parse options into a dataclass instance."""
options = {k: v for k, v in options.items() if k in dir(self.options_cls)}
return self.options_cls(**options)

def template_path(self, template_name: str) -> Path:
"""Get a `pathlib.Path` for the named template.
A `template_name` is the app-local name, e.g. `enrollment/success.html`.
Adapted from https://stackoverflow.com/a/75863472.
"""
for engine in template.engines.all():
for loader in engine.engine.template_loaders:
for origin in loader.get_template_sources(template_name):
path = Path(origin.name)
if path.exists():
return path
raise template.TemplateDoesNotExist(f"Could not find template: {template_name}")
3 changes: 2 additions & 1 deletion benefits/cli/management/commands/agency.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from benefits.cli.agency.create import Create
from benefits.cli.agency.list import List
from benefits.cli.commands import BenefitsCommand

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

help = __doc__
name = "agency"
subcommands = [List]
subcommands = [List, Create]

def __init__(self, stdout=None, stderr=None, no_color=False, force_color=False):
# make List the default_subcmd
Expand Down
49 changes: 49 additions & 0 deletions tests/pytest/cli/agency/test_create.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import pytest

from django.core.management.base import CommandError

from benefits.cli.agency.create import Create
from benefits.core.models import TransitAgency


@pytest.fixture
def cmd(cmd):
def call(*args, **kwargs):
return cmd(Create, *args, **kwargs)

return call


@pytest.mark.django_db
def test_call_no_slug(cmd):
with pytest.raises(CommandError, match="the following arguments are required: slug"):
cmd()


@pytest.mark.django_db
def test_call(cmd, model_TransitProcessor):
slug = "the-slug"

agency = TransitAgency.by_slug(slug)
assert agency is None

out, err = cmd(slug)

assert err == ""
assert "Creating new agency" in out
assert f"Agency created: {slug}" in out

agency = TransitAgency.by_slug(slug)
assert isinstance(agency, TransitAgency)
assert agency.transit_processor == model_TransitProcessor


@pytest.mark.django_db
def test_call_dupe(cmd):
slug = "the-slug"

# first time is OK
cmd(slug)
# again with the same slug, not OK
with pytest.raises(CommandError, match=f"TransitAgency with slug already exists: {slug}"):
cmd(slug)
5 changes: 5 additions & 0 deletions tests/pytest/cli/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,8 @@ def call(cls, *args, **kwargs):
return capfd.readouterr()

return call


@pytest.fixture(autouse=True)
def db_setup(model_TransitProcessor):
pass
21 changes: 20 additions & 1 deletion tests/pytest/cli/management/commands/test_agency.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import pytest

from benefits.cli.agency.create import Create
from benefits.cli.agency.list import List
from benefits.cli.management.commands.agency import Command


@pytest.fixture
def cmd(cmd):
def call(*args, **kwargs):
return cmd(Command, *args, **kwargs)

return call


@pytest.mark.django_db
def test_class():
assert Command.help == Command.__doc__
assert Command.name == "agency"
assert Command.subcommands == [List]

assert List in Command.subcommands
assert Create in Command.subcommands


@pytest.mark.django_db
Expand All @@ -21,3 +32,11 @@ def test_init():
list_cmd = getattr(cmd, "list")
assert isinstance(list_cmd, List)
assert cmd.default_handler == list_cmd.handle


@pytest.mark.django_db
def test_call(cmd):
out, err = cmd()

assert "No matching agencies" in out
assert err == ""

0 comments on commit 518fd02

Please sign in to comment.