Skip to content

Commit 04c18fd

Browse files
committed
feat: add --qe flag to dbx project add with --wagtail stacking
Adds --qe/-q to configure Queryable Encryption automatically on project creation, mirroring the --wagtail flag. Stacking --qe --wagtail selects medical_records.wagtail instead of medical_records.django in QE_INSTALLED_APPS. pymongocrypt is added to main dependencies when --qe is used.
1 parent 61564bb commit 04c18fd

3 files changed

Lines changed: 156 additions & 9 deletions

File tree

src/dbx_python_cli/commands/project.py

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,12 @@ def add_project(
231231
"-w/-W",
232232
help="Enable Wagtail CMS (default: False)",
233233
),
234+
add_qe: bool = typer.Option(
235+
False,
236+
"--qe/--no-qe",
237+
"-q/-Q",
238+
help="Enable Queryable Encryption (default: False)",
239+
),
234240
auto_install: bool = typer.Option(
235241
True,
236242
"--install/--no-install",
@@ -246,20 +252,23 @@ def add_project(
246252
"""
247253
Create a new Django project using bundled templates.
248254
Frontend is added by default. Use --no-frontend to skip frontend creation.
249-
Use --wagtail to enable Wagtail CMS.
255+
Use --wagtail to enable Wagtail CMS. Use --qe to enable Queryable Encryption.
256+
Flags can be stacked: --qe --wagtail enables a Wagtail site with QE.
250257
251258
Projects are created in base_dir/projects/ by default.
252259
If no name is provided, a random name is generated.
253260
254261
Examples::
255262
256-
dbx project add # Create with random name (includes frontend)
257-
dbx project add myproject # Create with explicit name (includes frontend)
263+
dbx project add # Create with random name (includes frontend)
264+
dbx project add myproject # Create with explicit name (includes frontend)
258265
dbx project add myproject --no-frontend # Create without frontend
259266
dbx project add myproject --wagtail # Create with Wagtail CMS enabled
260-
dbx project add -d ~/custom/path # Create with random name in custom directory
261-
dbx project add myproject -d ~/custom/path # Create in custom directory
262-
dbx project add myproject --base-dir ~/path/to/myproject # Create directly at ~/path/to/myproject
267+
dbx project add myproject --qe # Create with Queryable Encryption enabled
268+
dbx project add myproject --qe --wagtail # Create with QE + Wagtail
269+
dbx project add -d ~/custom/path # Create with random name in custom directory
270+
dbx project add myproject -d ~/custom/path # Create in custom directory
271+
dbx project add myproject --base-dir ~/path/to/myproject # Create directly at path
263272
"""
264273
# Normalize parameters when called programmatically (not via CLI).
265274
# When called directly, typer.Option/Argument defaults are OptionInfo/ArgumentInfo objects.
@@ -273,6 +282,8 @@ def add_project(
273282
add_frontend = True
274283
if not isinstance(add_wagtail, bool):
275284
add_wagtail = False
285+
if not isinstance(add_qe, bool):
286+
add_qe = False
276287
if not isinstance(auto_install, bool):
277288
auto_install = True
278289
if not isinstance(python_path_override, (str, type(None))):
@@ -453,7 +464,9 @@ def add_project(
453464
raise typer.Exit(code=1)
454465

455466
# Add pyproject.toml after project creation
456-
_create_pyproject_toml(project_path, name, settings_path, wagtail=add_wagtail)
467+
_create_pyproject_toml(
468+
project_path, name, settings_path, wagtail=add_wagtail, qe=add_qe
469+
)
457470

458471
# Enable Wagtail CMS if requested
459472
if add_wagtail:
@@ -466,6 +479,17 @@ def add_project(
466479
err=True,
467480
)
468481

482+
# Enable Queryable Encryption if requested
483+
if add_qe:
484+
typer.echo(f"🔐 Enabling Queryable Encryption for project '{name}'...")
485+
try:
486+
_enable_qe(project_path, name, wagtail=add_wagtail)
487+
except Exception as e:
488+
typer.echo(
489+
f"⚠️ Project created successfully, but QE setup failed: {e}",
490+
err=True,
491+
)
492+
469493
# Create frontend by default (unless --no-frontend is specified)
470494
if add_frontend:
471495
typer.echo(f"🎨 Adding frontend to project '{name}'...")
@@ -711,6 +735,7 @@ def _create_pyproject_toml(
711735
project_name: str,
712736
settings_path: str = "settings.base",
713737
wagtail: bool = False,
738+
qe: bool = False,
714739
):
715740
"""Create a pyproject.toml file for the Django project."""
716741
base_deps = [
@@ -721,6 +746,8 @@ def _create_pyproject_toml(
721746
]
722747
if wagtail:
723748
base_deps.append('"wagtail"')
749+
if qe:
750+
base_deps.append('"pymongocrypt"')
724751
deps_str = ",\n ".join(base_deps)
725752

726753
pyproject_content = f"""[build-system]
@@ -814,6 +841,30 @@ def _enable_wagtail(project_path: Path, project_name: str) -> None:
814841
f.write(wagtail_block)
815842

816843

844+
def _enable_qe(project_path: Path, project_name: str, wagtail: bool = False) -> None:
845+
"""Uncomment the QE block in settings and select the correct medical_records app."""
846+
settings_file = project_path / project_name / "settings" / f"{project_name}.py"
847+
if settings_file.exists():
848+
content = settings_file.read_text()
849+
content = content.replace(
850+
"# from .qe import * # noqa\n"
851+
"# INSTALLED_APPS += QE_INSTALLED_APPS # noqa: F405",
852+
"from .qe import * # noqa\n"
853+
"INSTALLED_APPS += QE_INSTALLED_APPS # noqa: F405",
854+
)
855+
settings_file.write_text(content)
856+
857+
if wagtail:
858+
qe_settings_file = project_path / project_name / "settings" / "qe.py"
859+
if qe_settings_file.exists():
860+
qe_content = qe_settings_file.read_text()
861+
qe_content = qe_content.replace(
862+
'"medical_records.django"',
863+
'"medical_records.wagtail"',
864+
)
865+
qe_settings_file.write_text(qe_content)
866+
867+
817868
def _setup_wagtail_initial_data(
818869
python_path: str,
819870
proj,

src/dbx_python_cli/templates/project_template/project_name/settings/qe.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from pymongo.encryption_options import AutoEncryptionOpts
44

55
QE_INSTALLED_APPS = [
6-
"medical_records",
6+
"medical_records.django",
77
]
88

99
DATABASES = {

tests/test_project_command.py

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,4 +479,100 @@ def test_project_run_uses_django_group_venv(tmp_path):
479479
call_args = call[0][0]
480480
if len(call_args) > 1 and "manage.py" in str(call_args):
481481
assert "django/.venv" in call_args[0]
482-
break
482+
483+
484+
def test_project_add_help_shows_qe():
485+
"""Test that the project add help shows the --qe flag."""
486+
result = runner.invoke(app, ["project", "add", "--help"])
487+
assert result.exit_code == 0
488+
output = strip_ansi(result.stdout)
489+
assert "--qe" in output
490+
491+
492+
def test_enable_qe_activates_settings(tmp_path):
493+
"""Test that _enable_qe uncomments the QE settings block."""
494+
from dbx_python_cli.commands.project import _enable_qe
495+
496+
settings_dir = tmp_path / "myproject" / "settings"
497+
settings_dir.mkdir(parents=True)
498+
settings_file = settings_dir / "myproject.py"
499+
settings_file.write_text(
500+
"# Queryable Encryption (QE) Configuration\n"
501+
"# Uncomment the two lines below to enable Queryable Encryption settings.\n"
502+
"# from .qe import * # noqa\n"
503+
"# INSTALLED_APPS += QE_INSTALLED_APPS # noqa: F405\n"
504+
)
505+
(settings_dir / "qe.py").write_text(
506+
'QE_INSTALLED_APPS = ["medical_records.django"]\n'
507+
)
508+
509+
_enable_qe(tmp_path, "myproject")
510+
511+
content = settings_file.read_text()
512+
assert "# from .qe import *" not in content
513+
assert "from .qe import * # noqa" in content
514+
assert "INSTALLED_APPS += QE_INSTALLED_APPS # noqa: F405" in content
515+
516+
517+
def test_enable_qe_uses_django_app_by_default(tmp_path):
518+
"""Test that _enable_qe leaves medical_records.django when wagtail=False."""
519+
from dbx_python_cli.commands.project import _enable_qe
520+
521+
settings_dir = tmp_path / "myproject" / "settings"
522+
settings_dir.mkdir(parents=True)
523+
(settings_dir / "myproject.py").write_text(
524+
"# from .qe import * # noqa\n"
525+
"# INSTALLED_APPS += QE_INSTALLED_APPS # noqa: F405\n"
526+
)
527+
qe_file = settings_dir / "qe.py"
528+
qe_file.write_text('QE_INSTALLED_APPS = ["medical_records.django"]\n')
529+
530+
_enable_qe(tmp_path, "myproject", wagtail=False)
531+
532+
assert '"medical_records.django"' in qe_file.read_text()
533+
534+
535+
def test_enable_qe_uses_wagtail_app_when_stacked(tmp_path):
536+
"""Test that _enable_qe replaces app with medical_records.wagtail when wagtail=True."""
537+
from dbx_python_cli.commands.project import _enable_qe
538+
539+
settings_dir = tmp_path / "myproject" / "settings"
540+
settings_dir.mkdir(parents=True)
541+
(settings_dir / "myproject.py").write_text(
542+
"# from .qe import * # noqa\n"
543+
"# INSTALLED_APPS += QE_INSTALLED_APPS # noqa: F405\n"
544+
)
545+
qe_file = settings_dir / "qe.py"
546+
qe_file.write_text('QE_INSTALLED_APPS = ["medical_records.django"]\n')
547+
548+
_enable_qe(tmp_path, "myproject", wagtail=True)
549+
550+
qe_content = qe_file.read_text()
551+
assert '"medical_records.wagtail"' in qe_content
552+
assert '"medical_records.django"' not in qe_content
553+
554+
555+
def test_create_pyproject_toml_includes_pymongocrypt_when_qe(tmp_path):
556+
"""Test that _create_pyproject_toml adds pymongocrypt to dependencies when qe=True."""
557+
from dbx_python_cli.commands.project import _create_pyproject_toml
558+
559+
_create_pyproject_toml(tmp_path, "myproject", qe=True)
560+
561+
content = (tmp_path / "pyproject.toml").read_text()
562+
deps_section = content[
563+
content.index("dependencies") : content.index("[project.optional-dependencies]")
564+
]
565+
assert '"pymongocrypt"' in deps_section
566+
567+
568+
def test_create_pyproject_toml_excludes_pymongocrypt_by_default(tmp_path):
569+
"""Test that _create_pyproject_toml does not add pymongocrypt by default."""
570+
from dbx_python_cli.commands.project import _create_pyproject_toml
571+
572+
_create_pyproject_toml(tmp_path, "myproject")
573+
574+
content = (tmp_path / "pyproject.toml").read_text()
575+
deps_section = content[
576+
content.index("dependencies") : content.index("[project.optional-dependencies]")
577+
]
578+
assert '"pymongocrypt"' not in deps_section

0 commit comments

Comments
 (0)