diff --git a/.gitignore b/.gitignore index 6c4b6f7..6955383 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ dist/ wheels/ *.egg-info _staticfiles/ +_logs/ +db.sqlite3 # Virtual environments .venv diff --git a/README.md b/README.md index 5983631..5b175f7 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@ This is our opinionated template for starting new Django projects. -It adds a few libraries I use in every Django project with reasonable starting configurations, -linting/CI rules, and a project layout that has worked for many Django projects. +It adds a few libraries useful for every Django project, with reasonable starting configurations. +Additionally, the repository has linting/CI rules and a project layout that has worked well for our many Django projects. ## License -This project is placed into the public domain ([CC0](https://creativecommons.org/public-domain/cc0/)) -so you may use it however you see fit. -You can clone this repository, use it as a template, or pick & choose what you like. +This project is placed into the public domain ([CC0](https://creativecommons.org/public-domain/cc0/)) so you may use it however you see fit. + +You can clone this repository, use it as a template, or pick & choose what you like. Attribution is appreciated but not required. Please note that the underlying libraries are under their own (MIT/BSD) licenses. @@ -19,8 +19,9 @@ If you are using this library as a baseline, there are a few steps you'll need t 1. Replace all instances of "djeff" with your project name. 2. Add a LICENSE -3. TODO 3. run `uv run pre-commit install` +4. Read through the various sections below to familiarize yourself with the setup. + A few of the libraries may require additional setup, documented under the **You:** steps below. ## File System Layout @@ -31,11 +32,18 @@ If you are using this library as a baseline, there are a few steps you'll need t ) - `templates/` - Django is configured to search this directory for your templates. You can also put templates within `/templates/` for any given app, but this layout keeps them all together. -Ignored: -- _staticfiles +Additionally, there are two directories that you will see after running your application. These are `.gitignore`d. + +- `_staticfiles` - Where Django will store the combined static files for serving. Do not modify files in this directory directly, instead modify the copies in `static`. +- `_logs` - The default destination of the log files, can be modified in `config/settings.py`. ## Tool Choices +- `ruff` - Ensure code quality. +- `uv` - Manage packages. +- `pre-commit` - Enforce repository standards. +- `just` - Run common tasks. + ## Django Plugins/Apps ### django-environ @@ -56,8 +64,6 @@ Efficiently serve static files alongside your application. **You:** Put your static files in `static/` & files that you need served at the root of your domain (like `robots.txt`) in `static/root`. -TODO: document whitenoise Compression setting - ### django-typer @@ -92,14 +98,19 @@ Provides an in-browser interface for inspecting Django views. ### django-structlog -Library +This library integrates [structured logging](https://www.structlog.org/en/stable/) with Django. -TODO: needs defaults +**We:** Provided default configuration that writes logs to the `logs/` directory. + +**You:** Modify the `LOGGING` config to reflect your application's name and desired log levels/types. + + ### django-allauth -Configured for email login. +Augment's django's built in `auth` with commonly-needed views for signup, email confirmation, etc. -#### Options +**We:** Configured for email login. + +**You:** Review settings & ensure that they meet your application's login needs. -TODO: prerolled Auth options diff --git a/_logs/.keep b/_logs/.keep new file mode 100644 index 0000000..e69de29 diff --git a/config/settings.py b/config/settings.py index 635c858..6e07904 100644 --- a/config/settings.py +++ b/config/settings.py @@ -1,4 +1,5 @@ import environ +import structlog import sys from pathlib import Path @@ -17,11 +18,12 @@ print("debug", DEBUG) if DEBUG: SECRET_KEY = env.str("SECRET_KEY", "needs-to-be-set-in-prod") + _DEFAULT_DB = env.db(default="sqlite:///" + str(BASE_DIR / "db.sqlite3")) else: SECRET_KEY = env.str("SECRET_KEY") + _DEFAULT_DB = env.db() -DATABASES = {"default": env.db()} - +DATABASES = {"default": _DEFAULT_DB} ALLOWED_HOSTS = [] INTERNAL_IPS = ["127.0.0.1"] @@ -44,7 +46,6 @@ INSTALLED_APPS = [ "allauth.account", "django_structlog", "django_typer", - "debug_toolbar", ] MIDDLEWARE = [ @@ -61,7 +62,7 @@ MIDDLEWARE = [ ] if DEBUG and not IS_TESTING: - INSTALLED_APPS += "debug_toolbar" + INSTALLED_APPS += ["debug_toolbar"] MIDDLEWARE.insert( 2, "debug_toolbar.middleware.DebugToolbarMiddleware", @@ -112,6 +113,10 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] +# This configures django-allauth with reasonably secure defaults +# for an email-based account. +# +# TODO: Document other common configurations. ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 1 ACCOUNT_EMAIL_VERIFICATION = "mandatory" ACCOUNT_EMAIL_VERIFICATION_BY_CODE_ENABLED = True @@ -129,6 +134,77 @@ ACCOUNT_USER_MODEL_USERNAME_FIELD = None # ACCOUNT_LOGIN_BY_CODE_REQUIRED = False # ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https" +# Logging Config --------- + +# default to not capturing data we don't know we need (re-enable as needed) +DJANGO_STRUCTLOG_IP_LOGGING_ENABLED = False +DJANGO_STRUCTLOG_USER_ID_FIELD = None + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "json_formatter": { + "()": structlog.stdlib.ProcessorFormatter, + "processor": structlog.processors.JSONRenderer(), + }, + "plain_console": { + "()": structlog.stdlib.ProcessorFormatter, + "processor": structlog.dev.ConsoleRenderer(), + }, + "key_value": { + "()": structlog.stdlib.ProcessorFormatter, + "processor": structlog.processors.KeyValueRenderer( + key_order=["timestamp", "level", "event", "logger"] + ), + }, + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "plain_console", + }, + "json_file": { + "class": "logging.handlers.WatchedFileHandler", + "filename": "_logs/log.json", + "formatter": "json_formatter", + }, + "flat_line_file": { + "class": "logging.handlers.WatchedFileHandler", + "filename": "_logs/flat.log", + "formatter": "key_value", + }, + }, + "loggers": { + "django_structlog": { + "handlers": ["console", "flat_line_file", "json_file"], + "level": "INFO", + }, + "djeff": { + "handlers": ["console", "flat_line_file", "json_file"], + "level": "INFO", + }, + }, +} + +structlog.configure( + processors=[ + structlog.contextvars.merge_contextvars, + structlog.stdlib.filter_by_level, + structlog.processors.TimeStamper(fmt="iso"), + structlog.stdlib.add_logger_name, + structlog.stdlib.add_log_level, + structlog.stdlib.PositionalArgumentsFormatter(), + structlog.processors.StackInfoRenderer(), + structlog.processors.format_exc_info, + structlog.processors.UnicodeDecoder(), + structlog.stdlib.ProcessorFormatter.wrap_for_formatter, + ], + logger_factory=structlog.stdlib.LoggerFactory(), + cache_logger_on_first_use=True, +) + + # Static File Config (per whitenoise) ----- # TODO: make configurable @@ -143,3 +219,4 @@ STATIC_URL = "static/" STATICFILES_DIRS = [BASE_DIR / "static"] # this directory is served at project root (for favicon.ico/robots.txt/etc.) WHITENOISE_ROOT = BASE_DIR / "static" / "root" +