Alright, I've been busy with some other projects and I didn't realize how many changes have piled up since the last update. I guess this is the benefit of using the framework myself — it's always moving forward! There are also some big changes in "security" and local dev, but I'll save those for the next update. I'll try to be succinct... here's what we've got: - [Automatic validation on model save](#automatic-validation-on-model-save) - [Stricter settings](#stricter-settings) - [Time zones, always](#time-zones-always) - [`plain.loginlink`](#plainloginlink) - [Template emails](#template-emails) - [Borrowing the `dd` function](#borrowing-the-dd-function) - [Switch from Poetry to UV](#switch-from-poetry-to-uv) - [Updates to `plain.vendor`](#updates-to-plainvendor) - [Next steps towards 1.0](#next-steps-towards-10) ## Automatic validation on model save I'm consistently surprised in Django when I save something manually, and later realize that it should have caused a `ValidationError`. There are plenty of good (?) reasons why Django works that way, but *surprises* in a framework feel like a bad thing, so I finally changed it! Now when you `save()` in Plain it will run `full_clean()` by default. ```python x = MyModel.objects.get(id=1) x.something = "changed!" x.save() ``` And if you want to save without validation, you can do that too. ```python x = MyModel.objects.get(id=1) x.something = "changed!" x.save(clean_and_validate=False) ``` Personally, I find that using models this way is not a bad thing, and that forms (where validation usually runs) aren't always necessary. ## Stricter settings When you want to define a new setting in `settings.py`, you have to prefix it with `APP_`. This adds some clarity between settings that are part of installed packages and settings that are custom to your app. ```python # settings.py INSTALLED_APPS = [ # ... ] AUTH_USER_MODEL = "users.User" AUTH_LOGIN_URL = "login" # Custom settings APP_GIT_SHA = os.environ.get("HEROKU_SLUG_COMMIT", "dev") APP_OPENAI_API_KEY = os.environ["OPENAI_API_KEY"] ``` Not only that, but now *Plain* can also tell the difference too! If a package is updated and a setting was *removed* or *renamed*, you'll immediately know it. When you add this to the *required* and *type-checked* settings, Plain can actually do a lot to validate your settings at startup.  ## Time zones, always The `USE_TZ` setting has been removed. It just seemed like there wasn't much reason to set this to `False` in a new app. Bad idea? Time will tell. ## `plain.loginlink` I was working on a project that needed "magic link" login, and decided to rough out a package for it. One of the visions for Plain is to separate packages per authentication method, so you can pick and choose the ones you actually need. Plain now has: - `plain.auth` (the base package) - `plain.passwords` - `plain.oauth` - `plain.loginlink` And I expect to add: - `plain.totp` - `plain.passkeys` ## Template emails For years I've been copying and pasting a class that rendered HTML emails (and optionally plaintext) from templates. It's simple, but I decided to formalize this pattern in `plain.mail`. ```python from plain.mail import TemplateEmail TemplateEmail( template="loginlink", subject="Your link to log in", to=["dave@example.com"], context={"link": "https://..."}, ).send() ``` ```html
Click here to log in.
``` Now packages can use a consistent pattern, giving users the option to customize the email templates. (And you can use it yourself too.) ## Borrowing the `dd` function One downside of using a [honcho-style all-in-one dev command](https://x.com/davegaeddert/status/1844440556749062270) is that it's harder to simply drop in a `import pdb; pdb.set_trace()` to debug, or even find a `print()` if there's a lot of output. I've always liked the [`dd` function in Laravel](https://laravel.com/docs/11.x/helpers#method-dd) and I thought it'd be interesting to just try it out.  (`ResponseException` makes this possible, where you can "abort" a request at any time, regardless of where you are in the stack.) ## Switch from Poetry to UV The starter kits now use [uv](https://github.com/astral-sh/uv). Honestly, uv is changing so fast that it feels early to make this a default, but it's the direction I hope the broader ecosystem goes. ## Updates to `plain.vendor` The `plain.vendor` package "vendors" CSS and JS files, typically from a CDN. This has existed for a while too, but I added support for sourcemaps and reworked the CLI a bit. More in this tweet if you're interested: [https://x.com/davegaeddert/status/1834288804246814963](https://x.com/davegaeddert/status/1834288804246814963) ## Next steps towards 1.0 As much as I'd like Plain to get some traction beyond myself, I'm not in a huge rush to get to 1.0 just yet. I'm still enjoying the pre-1.0 phase where I can make significant changes without seriously impacting anyone but myself. That said, the list of blocking changes is getting shorter and shorter, so we'll see how it goes!