Skip to main content

Model your data and store it in a database.

Project description

plain.models

Model your data and store it in a database.

Overview

# app/users/models.py
from plain import models
from plain.passwords.models import PasswordField


@models.register_model
class User(models.Model):
    email = models.EmailField()
    password = PasswordField()
    is_admin = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.email

Every model automatically includes an id field which serves as the primary key. The name id is reserved and can't be used for other fields.

Create, update, and delete instances of your models:

from .models import User


# Create a new user
user = User.query.create(
    email="test@example.com",
    password="password",
)

# Update a user
user.email = "new@example.com"
user.save()

# Delete a user
user.delete()

# Query for users
admin_users = User.query.filter(is_admin=True)

Database connection

To connect to a database, you can provide a DATABASE_URL environment variable:

DATABASE_URL=postgresql://user:password@localhost:5432/dbname

Or you can manually define the DATABASE setting:

# app/settings.py
DATABASE = {
    "ENGINE": "plain.models.backends.postgresql",
    "NAME": "dbname",
    "USER": "user",
    "PASSWORD": "password",
    "HOST": "localhost",
    "PORT": "5432",
}

Multiple backends are supported, including Postgres, MySQL, and SQLite.

Querying

Models come with a powerful query API through their QuerySet interface:

# Get all users
all_users = User.query.all()

# Filter users
admin_users = User.query.filter(is_admin=True)
recent_users = User.query.filter(created_at__gte=datetime.now() - timedelta(days=7))

# Get a single user
user = User.query.get(email="test@example.com")

# Complex queries with Q objects
from plain.models import Q
users = User.query.filter(
    Q(is_admin=True) | Q(email__endswith="@example.com")
)

# Ordering
users = User.query.order_by("-created_at")

# Limiting results
first_10_users = User.query.all()[:10]

For more advanced querying options, see the QuerySet class.

Migrations

Migrations track changes to your models and update the database schema accordingly:

# Create migrations for model changes
plain makemigrations

# Apply migrations to the database
plain migrate

# See migration status
plain models show-migrations

Migrations are Python files that describe database schema changes. They're stored in your app's migrations/ directory.

Fields

Plain provides many field types for different data:

from plain import models

class Product(models.Model):
    # Text fields
    name = models.CharField(max_length=200)
    description = models.TextField()

    # Numeric fields
    price = models.DecimalField(max_digits=10, decimal_places=2)
    quantity = models.IntegerField(default=0)

    # Boolean fields
    is_active = models.BooleanField(default=True)

    # Date and time fields
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    # Relationships
    category = models.ForeignKey("Category", on_delete=models.CASCADE)
    tags = models.ManyToManyField("Tag")

Common field types include:

Validation

Models can be validated before saving:

class User(models.Model):
    email = models.EmailField(unique=True)
    age = models.IntegerField()

    def clean(self):
        if self.age < 18:
            raise ValidationError("User must be 18 or older")

    def save(self, *args, **kwargs):
        self.full_clean()  # Runs validation
        super().save(*args, **kwargs)

Field-level validation happens automatically based on field types and constraints.

Indexes and constraints

Optimize queries and ensure data integrity with indexes and constraints:

class User(models.Model):
    email = models.EmailField()
    username = models.CharField(max_length=150)
    age = models.IntegerField()

    class Meta:
        indexes = [
            models.Index(fields=["email"]),
            models.Index(fields=["-created_at"], name="user_created_idx"),
        ]
        constraints = [
            models.UniqueConstraint(fields=["email", "username"], name="unique_user"),
            models.CheckConstraint(check=models.Q(age__gte=0), name="age_positive"),
        ]

Custom QuerySets

With the Manager functionality now merged into QuerySet, you can customize QuerySet classes to provide specialized query methods. There are several ways to use custom QuerySets:

Setting a default QuerySet for a model

Use Meta.queryset_class to set a custom QuerySet that will be used by Model.query:

class PublishedQuerySet(models.QuerySet):
    def published_only(self):
        return self.filter(status="published")

    def draft_only(self):
        return self.filter(status="draft")

@models.register_model
class Article(models.Model):
    title = models.CharField(max_length=200)
    status = models.CharField(max_length=20)

    class Meta:
        queryset_class = PublishedQuerySet

# Usage - all methods available on Article.objects
all_articles = Article.query.all()
published_articles = Article.query.published_only()
draft_articles = Article.query.draft_only()

Using custom QuerySets without formal attachment

You can also use custom QuerySets manually without setting them as the default:

class SpecialQuerySet(models.QuerySet):
    def special_filter(self):
        return self.filter(special=True)

# Create and use the QuerySet manually
special_qs = SpecialQuerySet(model=Article)
special_articles = special_qs.special_filter()

Using classmethods for convenience

For even cleaner API, add classmethods to your model:

@models.register_model
class Article(models.Model):
    title = models.CharField(max_length=200)
    status = models.CharField(max_length=20)

    @classmethod
    def published(cls):
        return PublishedQuerySet(model=cls).published_only()

    @classmethod
    def drafts(cls):
        return PublishedQuerySet(model=cls).draft_only()

# Usage
published_articles = Article.published()
draft_articles = Article.drafts()

Forms

Models integrate with Plain's form system:

from plain import forms
from .models import User

class UserForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ["email", "is_admin"]

# Usage
form = UserForm(data=request.data)
if form.is_valid():
    user = form.save()

Sharing fields across models

To share common fields across multiple models, use Python classes as mixins. The final, registered model must inherit directly from models.Model and the mixins should not.

from plain import models


# Regular Python class for shared fields
class TimestampedMixin:
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)


# Models inherit from the mixin AND models.Model
@models.register_model
class User(TimestampedMixin, models.Model):
    email = models.EmailField()
    password = PasswordField()
    is_admin = models.BooleanField(default=False)


@models.register_model
class Note(TimestampedMixin, models.Model):
    content = models.TextField(max_length=1024)
    liked = models.BooleanField(default=False)

Installation

Install the plain.models package from PyPI:

uv add plain.models

Then add to your INSTALLED_PACKAGES:

# app/settings.py
INSTALLED_PACKAGES = [
    ...
    "plain.models",
]

Project details


Release history Release notifications | RSS feed

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

plain_models-0.47.0.tar.gz (357.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

plain_models-0.47.0-py3-none-any.whl (406.1 kB view details)

Uploaded Python 3

File details

Details for the file plain_models-0.47.0.tar.gz.

File metadata

  • Download URL: plain_models-0.47.0.tar.gz
  • Upload date:
  • Size: 357.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.8.22

File hashes

Hashes for plain_models-0.47.0.tar.gz
Algorithm Hash digest
SHA256 410f3b523d0c9f1d8db58988338a94c49f155d92365b577b3fac1bb03fbb6dcf
MD5 f28f463d82c50180c7a0efc38d0af85f
BLAKE2b-256 b4dcaa5f35681200fa4ca5a5ce94f25f6f466aa73f377cf1524a46c56677809e

See more details on using hashes here.

File details

Details for the file plain_models-0.47.0-py3-none-any.whl.

File metadata

File hashes

Hashes for plain_models-0.47.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f406d561f9db7429f11e4db70c11e796ab76c7f6e0f6ed1004146888570a66f8
MD5 43f3dc189a97fe3fc32d8c67f9d411d4
BLAKE2b-256 2a02967057a6857edee29ae51b7ea32e1bdc1e7f8ef1bb400b8ec8050da585dd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page