Plain is headed towards 1.0! Subscribe for development updates →

Validate on save, stricter settings, and more

Development update by @davegaeddert • 2024-10-15

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

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.

x = MyModel.objects.get(id=1)
x.something = "changed!"
x.save()

And if you want to save without validation, you can do that too.

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.

# 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.

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.

from plain.mail import TemplateEmail

TemplateEmail(
    template="loginlink",
    subject="Your link to log in",
    to=["[email protected]"],
    context={"link": "https://..."},
).send()
<!-- templates/mail/loginlink.html -->
<p>Click <a href="{{ link }}">here</a> to log in.</p>

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 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 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. 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

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!