Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
django_mongodb_cli.egg-info/
django_mongodb_cli/__pycache__/
__pycache__
/src/
.idea
server.log
Expand Down
Empty file added demo/__init__.py
Empty file.
Empty file.
7 changes: 7 additions & 0 deletions demo/medical_records/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.contrib import admin
from .models import Patient


@admin.register(Patient)
class PatientAdmin(admin.ModelAdmin):
pass
6 changes: 6 additions & 0 deletions demo/medical_records/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class MedicalRecordsConfig(AppConfig):
default_auto_field = "django_mongodb_backend.fields.ObjectIdAutoField"
name = "demo.medical_records"
77 changes: 77 additions & 0 deletions demo/medical_records/management/commands/create_patient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import os
import random
from django.core.management.base import BaseCommand
from faker import Faker

from django_mongodb_demo.models import Patient, PatientRecord, Billing


class Command(BaseCommand):
help = "Create patients with embedded patient records and billing using Faker. Optionally set MONGODB_URI."

def add_arguments(self, parser):
parser.add_argument(
"num_patients", type=int, help="Number of patients to create"
)
parser.add_argument(
"--flush",
action="store_true",
help="Delete all existing patients before creating new ones",
)
parser.add_argument(
"--mongodb-uri",
type=str,
help="MongoDB connection URI to set as MONGODB_URI env var",
)

def handle(self, *args, **options):
fake = Faker()

num_patients = options["num_patients"]

# Set MONGODB_URI if provided
if options.get("mongodb_uri"):
os.environ["MONGODB_URI"] = options["mongodb_uri"]
self.stdout.write(
self.style.SUCCESS(f"MONGODB_URI set to: {options['mongodb_uri']}")
)

# Optionally flush
if options["flush"]:
Patient.objects.all().delete()
self.stdout.write(self.style.WARNING("Deleted all existing patients."))

for _ in range(num_patients):
# Create a Billing object
billing = Billing(
cc_type=fake.credit_card_provider(), cc_number=fake.credit_card_number()
)

# Create a PatientRecord object
record = PatientRecord(
ssn=fake.ssn(),
billing=billing,
bill_amount=round(random.uniform(50.0, 5000.0), 2),
)

# Create Patient
patient = Patient(
patient_name=fake.name(),
patient_id=random.randint(100000, 999999),
patient_record=record,
)
patient.save()

self.stdout.write(
self.style.SUCCESS(
f"Created Patient: {patient.patient_name} ({patient.patient_id})"
)
)
self.stdout.write(f" SSN: {record.ssn}")
self.stdout.write(f" Billing CC Type: {billing.cc_type}")
self.stdout.write(f" Billing CC Number: {billing.cc_number}")
self.stdout.write(f" Bill Amount: ${record.bill_amount}")

self.stdout.write(
self.style.SUCCESS(f"Successfully created {num_patients} patient(s).")
)
27 changes: 27 additions & 0 deletions demo/medical_records/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from django.db import models
from django_mongodb_backend.models import EmbeddedModel
from django_mongodb_backend.fields import (
EmbeddedModelField,
EncryptedEmbeddedModelField,
EncryptedCharField,
)


class Patient(models.Model):
patient_name = models.CharField(max_length=255)
patient_id = models.BigIntegerField()
patient_record = EmbeddedModelField("PatientRecord")

def __str__(self):
return f"{self.patient_name} ({self.patient_id})"


class PatientRecord(EmbeddedModel):
ssn = EncryptedCharField(max_length=11)
billing = EncryptedEmbeddedModelField("Billing")
bill_amount = models.DecimalField(max_digits=10, decimal_places=2)


class Billing(EmbeddedModel):
cc_type = models.CharField(max_length=50)
cc_number = models.CharField(max_length=20)
14 changes: 14 additions & 0 deletions demo/medical_records/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from django.test import TestCase
from .models import Author, Article


class DemoTest(TestCase):
def test_create_author_and_article(self):
author = Author.objects.create(name="Alice", email="alice@example.com")
article = Article.objects.create(
title="Hello MongoDB",
slug="hello-mongodb",
author=author,
content="Testing MongoDB backend.",
)
self.assertEqual(article.author.name, "Alice")
7 changes: 7 additions & 0 deletions demo/medical_records/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.urls import path
from . import views

urlpatterns = [
path("", views.article_list, name="article_list"),
path("article/<slug:slug>/", views.article_detail, name="article_detail"),
]
15 changes: 15 additions & 0 deletions demo/medical_records/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django.shortcuts import render, get_object_or_404
from .models import Article


def article_list(request):
articles = Article.objects.all().order_by("-published_at")
return render(request, "demo/article_list.html", {"articles": articles})


def article_detail(request, slug):
article = get_object_or_404(Article, slug=slug)
comments = article.comments.all().order_by("-created_at")
return render(
request, "demo/article_detail.html", {"article": article, "comments": comments}
)
2 changes: 1 addition & 1 deletion django_mongodb_cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
app = typer.Typer(help="Manage Django apps.")


@app.command("create")
@app.command("add")
def add_app(name: str, project_name: str, directory: Path = Path(".")):
"""
Create a new Django app inside an existing project using bundled templates.
Expand Down
121 changes: 114 additions & 7 deletions django_mongodb_cli/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import subprocess
import importlib.resources as resources
import os
import sys
from .frontend import add_frontend as _add_frontend
from .utils import Repo

Expand Down Expand Up @@ -38,7 +39,45 @@ def add_project(
name,
]
typer.echo(f"📦 Creating project: {name}")
subprocess.run(cmd, check=True)

# Run django-admin in a way that surfaces a clean, user-friendly error
# instead of a full Python traceback when Django is missing or
# misconfigured in the current environment.
try:
result = subprocess.run(
cmd,
check=False,
capture_output=True,
text=True,
)
except FileNotFoundError:
typer.echo(
"❌ 'django-admin' command not found. Make sure Django is installed "
"in this environment and that 'django-admin' is on your PATH.",
err=True,
)
raise typer.Exit(code=1)

if result.returncode != 0:
# Try to show a concise reason (e.g. "ModuleNotFoundError: No module named 'django'")
reason = None
if result.stderr:
lines = [
line.strip() for line in result.stderr.splitlines() if line.strip()
]
if lines:
reason = lines[-1]

typer.echo(
"❌ Failed to create project using django-admin. "
"This usually means Django is not installed or is misconfigured "
"in the current Python environment.",
err=True,
)
if reason:
typer.echo(f" Reason: {reason}", err=True)

raise typer.Exit(code=result.returncode)

# Add pyproject.toml after project creation
_create_pyproject_toml(project_path, name)
Expand Down Expand Up @@ -106,15 +145,83 @@ def _create_pyproject_toml(project_path: Path, project_name: str):

@project.command("remove")
def remove_project(name: str, directory: Path = Path(".")):
"""
Delete a Django project by name.
"""Delete a Django project by name.

This will first attempt to uninstall the project package using pip in the
current Python environment, then remove the project directory.
"""
target = directory / name
if target.exists() and target.is_dir():
shutil.rmtree(target)
typer.echo(f"🗑️ Removed project {name}")
else:

if not target.exists() or not target.is_dir():
typer.echo(f"❌ Project {name} does not exist.", err=True)
return

# Try to uninstall the package from the current environment before
# removing the project directory. Failures here are non-fatal so that
# filesystem cleanup still proceeds.
uninstall_cmd = [sys.executable, "-m", "pip", "uninstall", "-y", name]
typer.echo(f"📦 Uninstalling project package '{name}' with pip")
try:
result = subprocess.run(uninstall_cmd, check=False)
if result.returncode != 0:
typer.echo(
f"⚠️ pip uninstall exited with code {result.returncode}. "
"Proceeding to remove project files.",
err=True,
)
except FileNotFoundError:
typer.echo(
"⚠️ Could not run pip to uninstall the project package. "
"Proceeding to remove project files.",
err=True,
)

shutil.rmtree(target)
typer.echo(f"🗑️ Removed project {name}")


@project.command("install")
def install_project(name: str, directory: Path = Path(".")):
"""Install a generated Django project by running ``pip install .`` in its directory.

Example:
dm project install qe
"""
project_path = directory / name

if not project_path.exists() or not project_path.is_dir():
typer.echo(
f"❌ Project '{name}' does not exist at {project_path}.",
err=True,
)
raise typer.Exit(code=1)

typer.echo(f"📦 Installing project '{name}' with pip (cwd={project_path})")

# Use the current Python interpreter to ensure we install into the
# same environment that is running the CLI.
cmd = [sys.executable, "-m", "pip", "install", "."]
try:
result = subprocess.run(
cmd,
cwd=project_path,
check=False,
)
except FileNotFoundError:
typer.echo(
"❌ Could not run pip. Make sure Python and pip are available in this environment.",
err=True,
)
raise typer.Exit(code=1)

if result.returncode != 0:
typer.echo(
f"❌ pip install failed with exit code {result.returncode}.",
err=True,
)
raise typer.Exit(code=result.returncode)

typer.echo(f"✅ Successfully installed project '{name}'")


def _django_manage_command(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
from pymongo.encryption_options import AutoEncryptionOpts
from bson import ObjectId

import os

# Queryable Encryption
INSTALLED_APPS += [ # noqa
"django_mongodb_backend",
"django_mongodb_demo",
"demo.medical_records",
]

DATABASES["encrypted"] = { # noqa
Expand All @@ -22,6 +24,8 @@
}
},
key_vault_namespace="{{ project_name }}_encrypted.__keyVault",
crypt_shared_lib_path=os.getenv("CRYPT_SHARED_LIB_PATH"),
crypt_shared_lib_required=True,
),
},
}
Expand Down
Loading