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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ name: CI

on:
pull_request:
push:
branches:
- main
workflow_dispatch:

jobs:
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:

permissions:
contents: write
packages: write

jobs:
generate-release:
Expand All @@ -23,7 +24,7 @@ jobs:
id: release
shell: bash
run: |
RELEASE_VERSION=${GITHUB_REF:11}
RELEASE_VERSION=${GITHUB_REF:10}

IS_PRERELEASE=false
if [[ "$RELEASE_VERSION" == *dev* ]]; then
Expand All @@ -46,7 +47,7 @@ jobs:
args: -vv --latest --no-exec --github-repo ${{ github.repository }} --strip all
env:
GIT_CLIFF__REMOTE__GITHUB__OWNER: toggle-corp
GIT_CLIFF__REMOTE__GITHUB__REPO: toggle-django-utils
GIT_CLIFF__REMOTE__GITHUB__REPO: banjo-utils

- name: Create Github Release
uses: softprops/action-gh-release@v2
Expand Down
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[submodule "fugit"]
path = fugit
url = https://github.com/toggle-corp/fugit.git
branch = v0.1.1
branch = v0.1.6
47 changes: 34 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# toggle-django-utils
# Banjo utils

Reusable Django utilities and management commands for Toggle projects.

Expand All @@ -8,43 +8,42 @@ Reusable Django utilities and management commands for Toggle projects.

- Shared management command: `wait_for_resources`
— Wait for database, Redis, Minio (S3) resources to be available before startup
- Create Initial Users: `create_initial_users`
- Create Users with specified roles and permissions, useful to populate the database with default users during development or testing

---

## Installation

**Using [uv](https://github.com/astral-sh/uv):**
```bash
uv pip install "git+ssh://git@github.com/toggle-corp/toggle-django-utils.git@main"
uv pip install "git+https://github.com/toggle-corp/banjo-utils.git@v0.1.0"
```

Or add to your `pyproject.toml`:
```toml
[project]
dependencies = [
"toggle-django-utils",
"banjo-utils",
]

[tool.uv.sources]
toggle-django-utils = { git = "https://github.com/toggle-corp/toggle-django-utils", branch = "main" }
banjo-utils = { git = "https://github.com/toggle-corp/banjo-utils", tag = "v0.1.0" }
```

---

## Setup in Django

1. **Add to `INSTALLED_APPS` in your Django project's `settings.py`:**
- **Add to `INSTALLED_APPS` in your Django project's `settings.py`:**

```python
INSTALLED_APPS = [
# ... your other apps ...
"toggle_django_utils",
"banjo_utils",
]
```

2. (Optional) If your `settings.py` uses custom configs, ensure `"toggle_django_utils"` remains in the app list.


---

## Usage
Expand All @@ -55,15 +54,32 @@ python manage.py wait_for_resources --db --redis
```

**Command options:**
- `--db`      Wait for database
- `--redis`   Wait for Redis server
- `--minio`   Wait for Minio (S3 storage)
- `--timeout`   Set max wait time (seconds)
- `--db`: Wait for database
- `--redis`: Wait for Redis server
- `--minio`: Wait for Minio (S3 storage)
- `--timeout`: Set max wait time (seconds)

**Examples:**
```bash
python manage.py wait_for_resources --db --redis
python manage.py wait_for_resources --timeout 300 --minio
python manage.py create_initial_users --users-json="
[
{
"username": "admin",
"email": "test@example.com",
"password": "admin123",
"is_superuser": true,
"is_staff": true
},
{
"username": "user1",
"email": "user1@gmail.com",
"password": "user123",
"is_superuser": false,
"is_staff": false
}
]'
```

---
Expand All @@ -83,6 +99,11 @@ python manage.py wait_for_resources --timeout 300 --minio
```bash
uv run --all-groups --all-extras pytest
```
4. Run commands for example project
```bash
uv run --all-groups --all-extras python example/manage.py runserver
uv run --all-groups --all-extras python example/manage.py wait_for_resources --db --redis
```

---

Expand Down
2 changes: 1 addition & 1 deletion TODOS.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
- We can use something like this inside <settings.py>

```
TOGGLE_DJANGO_UTILS_CONFIG = {
BANJO_UTILS_CONFIG = {
"WAIT_FOR_RESOURCES": {
"ALIAS": {
"dev": ["db", "redis", "minio"],
Expand Down
4 changes: 2 additions & 2 deletions example/main/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"toggle_django_utils",
"banjo_utils",
]

MIDDLEWARE = [
Expand All @@ -26,7 +26,7 @@
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": ":memory:",
"NAME": BASE_DIR / "db.sqlite3",
},
}

Expand Down
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ requires = [
]

[project]
name = "toggle-django-utils"
name = "banjo-utils"
version = "0.1.0"
description = "Shared Django utilities for Toggle projects"
readme = "README.md"
Expand Down Expand Up @@ -36,12 +36,12 @@ test = [
]

[project.urls]
Homepage = "https://github.com/toggle-corp/toggle-django-utils"
Repository = "https://github.com/toggle-corp/toggle-django-utils"
Issues = "https://github.com/toggle-corp/toggle-django-utils/issues"
Homepage = "https://github.com/toggle-corp/banjo-utils"
Repository = "https://github.com/toggle-corp/banjo-utils"
Issues = "https://github.com/toggle-corp/banjo-utils/issues"

[tool.hatch.build.targets.wheel]
packages = ["src/toggle_django_utils"]
packages = ["src/banjo_utils"]

[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["E402"]
Expand Down
File renamed without changes.
7 changes: 7 additions & 0 deletions src/banjo_utils/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.apps import AppConfig


class BanjoUtilsConfig(AppConfig):
name = "banjo_utils"
verbose_name = "Banjo Utils"
default_auto_field = "django.db.models.BigAutoField"
96 changes: 96 additions & 0 deletions src/banjo_utils/management/commands/create_initial_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from __future__ import annotations

import json
import pathlib
import typing
from typing import Any, override

from django.contrib.auth import get_user_model
from django.contrib.auth.hashers import identify_hasher
from django.core.management.base import BaseCommand, CommandError

if typing.TYPE_CHECKING:
from django.core.management.base import CommandParser


class Command(BaseCommand):
help = "Create/update users using JSON input"

@override
def add_arguments(self, parser: CommandParser):
parser.add_argument(
"--users-json",
type=str,
required=True,
help="JSON string containing list of users",
)

def is_hashed(self, password: str) -> bool:
try:
identify_hasher(password)
return True
except Exception:
return False

def load_json_string(self, json_str: str) -> list[dict[str, Any]]:
try:
data = json.loads(json_str)
if not isinstance(data, list):
raise ValueError("JSON must be a list of users")
return data
except Exception as e:
raise CommandError(f"Invalid JSON: {e}") from e

def load_json_file(self, path: str) -> list[dict[str, Any]]:
try:
with pathlib.Path(path).open(encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, list):
raise ValueError("JSON must be a list of users")
return data
except Exception as e:
raise CommandError(f"Error reading file {path}: {e}") from e

@override
def handle(self, *_, **options: Any):
User = get_user_model()
user_data = self.load_json_string(options["users_json"])

if not user_data:
raise CommandError("No users provided")

for user_info in user_data:
email = user_info.pop("email", None)
username = user_info.pop("username", email)
password = user_info.pop("password", None)

if not email or not password:
self.stdout.write(
self.style.WARNING(
f"Skipping user (missing email/password): {user_info}",
),
)
continue

defaults = {k: v for k, v in user_info.items()}
user, created = User.objects.update_or_create(
username=username,
email=email,
defaults=defaults,
)

if self.is_hashed(password):
user.password = password
else:
user.set_password(password)

user.save()

if created:
self.stdout.write(
self.style.SUCCESS(f"Created user: {email}"),
)
else:
self.stdout.write(
self.style.SUCCESS(f"Updated user: {email}"),
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from redis.exceptions import ConnectionError as RedisConnectionError
from typing_extensions import override

from toggle_django_utils.utils.retry import RetryHelper
from banjo_utils.utils.retry import RetryHelper


class TimeoutException(Exception): ...
Expand Down
File renamed without changes.
7 changes: 0 additions & 7 deletions src/toggle_django_utils/apps.py

This file was deleted.

2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def pytest_configure():
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"toggle_django_utils",
"banjo_utils",
],
MIDDLEWARE=[
"django.contrib.sessions.middleware.SessionMiddleware",
Expand Down
59 changes: 59 additions & 0 deletions tests/test_create_initial_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import json

import pytest
from django.contrib.auth import get_user_model
from django.core.management import call_command

User = get_user_model()


@pytest.mark.django_db
def test_create_users_command():
users_json = json.dumps(
[
{
"username": "admin",
"email": "admin@example.com",
"password": "plainpassword",
"is_superuser": True,
"is_staff": True,
},
{
"username": "guest",
"email": "guest@example.com",
"password": "pbkdf2_sha256$600000$example$hashhere",
"is_superuser": False,
"is_staff": False,
},
{
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"password": "pbkdf2_sha256$600000$example$hashhere",
"is_superuser": False,
"is_staff": False,
},
],
)

call_command("create_initial_users", users_json=users_json)

admin_user: User = User.objects.get(username="admin")
assert admin_user.email == "admin@example.com" # type: ignore[reportUnknownMemberType]
assert admin_user.is_superuser is True # type: ignore[reportUnknownMemberType]
assert admin_user.is_staff is True # type: ignore[reportUnknownMemberType]

# password should be hashed, not equal to the plain text
assert admin_user.check_password("plainpassword") is True

guest_user: User = User.objects.get(username="guest")
assert guest_user.email == "guest@example.com" # type: ignore[reportUnknownMemberType]
assert guest_user.is_superuser is False # type: ignore[reportUnknownMemberType]
assert guest_user.is_staff is False # type: ignore[reportUnknownMemberType]
assert guest_user.check_password("pbkdf2_sha256$600000$example$hashhere") is False # type: ignore[reportUnknownMemberType]

# The user with email as username should be created with email as username
john_user: User = User.objects.get(email="john@example.com")
assert john_user.username == john_user.email # type: ignore[reportUnknownMemberType]
assert john_user.first_name == "John" # type: ignore[reportUnknownMemberType]
assert john_user.last_name == "Doe" # type: ignore[reportUnknownMemberType]
Loading
Loading