plain.models 0.39.2__tar.gz → 0.40.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plain_models-0.40.1/PKG-INFO +312 -0
- plain_models-0.40.1/plain/models/AGENTS.md +4 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/CHANGELOG.md +26 -0
- plain_models-0.40.1/plain/models/README.md +301 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/base/creation.py +1 -1
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/cli.py +19 -118
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/fields/related_descriptors.py +9 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/migrations/executor.py +13 -110
- {plain_models-0.39.2 → plain_models-0.40.1}/pyproject.toml +2 -2
- plain_models-0.39.2/PKG-INFO +0 -147
- plain_models-0.39.2/plain/models/README.md +0 -136
- {plain_models-0.39.2 → plain_models-0.40.1}/.gitignore +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/LICENSE +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/README.md +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/__init__.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/aggregates.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/__init__.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/base/__init__.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/base/base.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/base/client.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/base/features.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/base/introspection.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/base/operations.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/base/schema.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/base/validation.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/ddl_references.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/mysql/__init__.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/mysql/base.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/mysql/client.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/mysql/compiler.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/mysql/creation.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/mysql/features.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/mysql/introspection.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/mysql/operations.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/mysql/schema.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/mysql/validation.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/postgresql/__init__.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/postgresql/base.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/postgresql/client.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/postgresql/creation.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/postgresql/features.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/postgresql/introspection.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/postgresql/operations.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/postgresql/schema.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/sqlite3/__init__.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/sqlite3/_functions.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/sqlite3/base.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/sqlite3/client.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/sqlite3/creation.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/sqlite3/features.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/sqlite3/introspection.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/sqlite3/operations.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/sqlite3/schema.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backends/utils.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backups/__init__.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backups/cli.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backups/clients.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/backups/core.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/base.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/config.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/connections.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/constants.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/constraints.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/database_url.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/db.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/default_settings.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/deletion.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/entrypoints.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/enums.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/exceptions.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/expressions.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/fields/__init__.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/fields/json.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/fields/mixins.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/fields/related.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/fields/related_lookups.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/fields/reverse_related.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/forms.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/functions/__init__.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/functions/comparison.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/functions/datetime.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/functions/math.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/functions/mixins.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/functions/text.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/functions/window.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/indexes.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/lookups.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/manager.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/migrations/__init__.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/migrations/autodetector.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/migrations/exceptions.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/migrations/graph.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/migrations/loader.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/migrations/migration.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/migrations/operations/__init__.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/migrations/operations/base.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/migrations/operations/fields.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/migrations/operations/models.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/migrations/operations/special.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/migrations/optimizer.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/migrations/questioner.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/migrations/recorder.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/migrations/serializer.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/migrations/state.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/migrations/utils.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/migrations/writer.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/options.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/otel.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/preflight.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/query.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/query_utils.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/registry.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/sql/__init__.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/sql/compiler.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/sql/constants.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/sql/datastructures.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/sql/query.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/sql/subqueries.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/sql/where.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/test/__init__.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/test/pytest.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/test/utils.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/transaction.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/plain/models/utils.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/tests/app/examples/migrations/0001_initial.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/tests/app/examples/migrations/__init__.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/tests/app/examples/models.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/tests/app/settings.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/tests/app/urls.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/tests/test_database_url.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/tests/test_delete_behaviors.py +0 -0
- {plain_models-0.39.2 → plain_models-0.40.1}/tests/test_models.py +0 -0
@@ -0,0 +1,312 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: plain.models
|
3
|
+
Version: 0.40.1
|
4
|
+
Summary: Model your data and store it in a database.
|
5
|
+
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
6
|
+
License-File: LICENSE
|
7
|
+
Requires-Python: >=3.11
|
8
|
+
Requires-Dist: plain<1.0.0
|
9
|
+
Requires-Dist: sqlparse>=0.3.1
|
10
|
+
Description-Content-Type: text/markdown
|
11
|
+
|
12
|
+
# plain.models
|
13
|
+
|
14
|
+
**Model your data and store it in a database.**
|
15
|
+
|
16
|
+
- [Overview](#overview)
|
17
|
+
- [Database connection](#database-connection)
|
18
|
+
- [Querying](#querying)
|
19
|
+
- [Migrations](#migrations)
|
20
|
+
- [Fields](#fields)
|
21
|
+
- [Validation](#validation)
|
22
|
+
- [Indexes and constraints](#indexes-and-constraints)
|
23
|
+
- [Managers](#managers)
|
24
|
+
- [Forms](#forms)
|
25
|
+
- [Sharing fields across models](#sharing-fields-across-models)
|
26
|
+
- [Installation](#installation)
|
27
|
+
|
28
|
+
## Overview
|
29
|
+
|
30
|
+
```python
|
31
|
+
# app/users/models.py
|
32
|
+
from plain import models
|
33
|
+
from plain.passwords.models import PasswordField
|
34
|
+
|
35
|
+
|
36
|
+
@models.register_model
|
37
|
+
class User(models.Model):
|
38
|
+
email = models.EmailField()
|
39
|
+
password = PasswordField()
|
40
|
+
is_admin = models.BooleanField(default=False)
|
41
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
42
|
+
|
43
|
+
def __str__(self):
|
44
|
+
return self.email
|
45
|
+
```
|
46
|
+
|
47
|
+
Every model automatically includes an `id` field which serves as the primary
|
48
|
+
key. The name `id` is reserved and can't be used for other fields.
|
49
|
+
|
50
|
+
Create, update, and delete instances of your models:
|
51
|
+
|
52
|
+
```python
|
53
|
+
from .models import User
|
54
|
+
|
55
|
+
|
56
|
+
# Create a new user
|
57
|
+
user = User.objects.create(
|
58
|
+
email="test@example.com",
|
59
|
+
password="password",
|
60
|
+
)
|
61
|
+
|
62
|
+
# Update a user
|
63
|
+
user.email = "new@example.com"
|
64
|
+
user.save()
|
65
|
+
|
66
|
+
# Delete a user
|
67
|
+
user.delete()
|
68
|
+
|
69
|
+
# Query for users
|
70
|
+
admin_users = User.objects.filter(is_admin=True)
|
71
|
+
```
|
72
|
+
|
73
|
+
## Database connection
|
74
|
+
|
75
|
+
To connect to a database, you can provide a `DATABASE_URL` environment variable:
|
76
|
+
|
77
|
+
```sh
|
78
|
+
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
|
79
|
+
```
|
80
|
+
|
81
|
+
Or you can manually define the `DATABASE` setting:
|
82
|
+
|
83
|
+
```python
|
84
|
+
# app/settings.py
|
85
|
+
DATABASE = {
|
86
|
+
"ENGINE": "plain.models.backends.postgresql",
|
87
|
+
"NAME": "dbname",
|
88
|
+
"USER": "user",
|
89
|
+
"PASSWORD": "password",
|
90
|
+
"HOST": "localhost",
|
91
|
+
"PORT": "5432",
|
92
|
+
}
|
93
|
+
```
|
94
|
+
|
95
|
+
Multiple backends are supported, including Postgres, MySQL, and SQLite.
|
96
|
+
|
97
|
+
## Querying
|
98
|
+
|
99
|
+
Models come with a powerful query API through their [`Manager`](./manager.py#Manager) interface:
|
100
|
+
|
101
|
+
```python
|
102
|
+
# Get all users
|
103
|
+
all_users = User.objects.all()
|
104
|
+
|
105
|
+
# Filter users
|
106
|
+
admin_users = User.objects.filter(is_admin=True)
|
107
|
+
recent_users = User.objects.filter(created_at__gte=datetime.now() - timedelta(days=7))
|
108
|
+
|
109
|
+
# Get a single user
|
110
|
+
user = User.objects.get(email="test@example.com")
|
111
|
+
|
112
|
+
# Complex queries with Q objects
|
113
|
+
from plain.models import Q
|
114
|
+
users = User.objects.filter(
|
115
|
+
Q(is_admin=True) | Q(email__endswith="@example.com")
|
116
|
+
)
|
117
|
+
|
118
|
+
# Ordering
|
119
|
+
users = User.objects.order_by("-created_at")
|
120
|
+
|
121
|
+
# Limiting results
|
122
|
+
first_10_users = User.objects.all()[:10]
|
123
|
+
```
|
124
|
+
|
125
|
+
For more advanced querying options, see the [`QuerySet`](./query.py#QuerySet) class.
|
126
|
+
|
127
|
+
## Migrations
|
128
|
+
|
129
|
+
Migrations track changes to your models and update the database schema accordingly:
|
130
|
+
|
131
|
+
```bash
|
132
|
+
# Create migrations for model changes
|
133
|
+
plain makemigrations
|
134
|
+
|
135
|
+
# Apply migrations to the database
|
136
|
+
plain migrate
|
137
|
+
|
138
|
+
# See migration status
|
139
|
+
plain models show-migrations
|
140
|
+
```
|
141
|
+
|
142
|
+
Migrations are Python files that describe database schema changes. They're stored in your app's `migrations/` directory.
|
143
|
+
|
144
|
+
## Fields
|
145
|
+
|
146
|
+
Plain provides many field types for different data:
|
147
|
+
|
148
|
+
```python
|
149
|
+
from plain import models
|
150
|
+
|
151
|
+
class Product(models.Model):
|
152
|
+
# Text fields
|
153
|
+
name = models.CharField(max_length=200)
|
154
|
+
description = models.TextField()
|
155
|
+
|
156
|
+
# Numeric fields
|
157
|
+
price = models.DecimalField(max_digits=10, decimal_places=2)
|
158
|
+
quantity = models.IntegerField(default=0)
|
159
|
+
|
160
|
+
# Boolean fields
|
161
|
+
is_active = models.BooleanField(default=True)
|
162
|
+
|
163
|
+
# Date and time fields
|
164
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
165
|
+
updated_at = models.DateTimeField(auto_now=True)
|
166
|
+
|
167
|
+
# Relationships
|
168
|
+
category = models.ForeignKey("Category", on_delete=models.CASCADE)
|
169
|
+
tags = models.ManyToManyField("Tag")
|
170
|
+
```
|
171
|
+
|
172
|
+
Common field types include:
|
173
|
+
|
174
|
+
- [`CharField`](./fields/__init__.py#CharField)
|
175
|
+
- [`TextField`](./fields/__init__.py#TextField)
|
176
|
+
- [`IntegerField`](./fields/__init__.py#IntegerField)
|
177
|
+
- [`DecimalField`](./fields/__init__.py#DecimalField)
|
178
|
+
- [`BooleanField`](./fields/__init__.py#BooleanField)
|
179
|
+
- [`DateTimeField`](./fields/__init__.py#DateTimeField)
|
180
|
+
- [`EmailField`](./fields/__init__.py#EmailField)
|
181
|
+
- [`URLField`](./fields/__init__.py#URLField)
|
182
|
+
- [`UUIDField`](./fields/__init__.py#UUIDField)
|
183
|
+
|
184
|
+
## Validation
|
185
|
+
|
186
|
+
Models can be validated before saving:
|
187
|
+
|
188
|
+
```python
|
189
|
+
class User(models.Model):
|
190
|
+
email = models.EmailField(unique=True)
|
191
|
+
age = models.IntegerField()
|
192
|
+
|
193
|
+
def clean(self):
|
194
|
+
if self.age < 18:
|
195
|
+
raise ValidationError("User must be 18 or older")
|
196
|
+
|
197
|
+
def save(self, *args, **kwargs):
|
198
|
+
self.full_clean() # Runs validation
|
199
|
+
super().save(*args, **kwargs)
|
200
|
+
```
|
201
|
+
|
202
|
+
Field-level validation happens automatically based on field types and constraints.
|
203
|
+
|
204
|
+
## Indexes and constraints
|
205
|
+
|
206
|
+
Optimize queries and ensure data integrity with indexes and constraints:
|
207
|
+
|
208
|
+
```python
|
209
|
+
class User(models.Model):
|
210
|
+
email = models.EmailField()
|
211
|
+
username = models.CharField(max_length=150)
|
212
|
+
age = models.IntegerField()
|
213
|
+
|
214
|
+
class Meta:
|
215
|
+
indexes = [
|
216
|
+
models.Index(fields=["email"]),
|
217
|
+
models.Index(fields=["-created_at"], name="user_created_idx"),
|
218
|
+
]
|
219
|
+
constraints = [
|
220
|
+
models.UniqueConstraint(fields=["email", "username"], name="unique_user"),
|
221
|
+
models.CheckConstraint(check=models.Q(age__gte=0), name="age_positive"),
|
222
|
+
]
|
223
|
+
```
|
224
|
+
|
225
|
+
## Managers
|
226
|
+
|
227
|
+
[`Manager`](./manager.py#Manager) objects provide the interface for querying models:
|
228
|
+
|
229
|
+
```python
|
230
|
+
class PublishedManager(models.Manager):
|
231
|
+
def get_queryset(self):
|
232
|
+
return super().get_queryset().filter(status="published")
|
233
|
+
|
234
|
+
class Article(models.Model):
|
235
|
+
title = models.CharField(max_length=200)
|
236
|
+
status = models.CharField(max_length=20)
|
237
|
+
|
238
|
+
# Default manager
|
239
|
+
objects = models.Manager()
|
240
|
+
|
241
|
+
# Custom manager
|
242
|
+
published = PublishedManager()
|
243
|
+
|
244
|
+
# Usage
|
245
|
+
all_articles = Article.objects.all()
|
246
|
+
published_articles = Article.published.all()
|
247
|
+
```
|
248
|
+
|
249
|
+
## Forms
|
250
|
+
|
251
|
+
Models integrate with Plain's form system:
|
252
|
+
|
253
|
+
```python
|
254
|
+
from plain import forms
|
255
|
+
from .models import User
|
256
|
+
|
257
|
+
class UserForm(forms.ModelForm):
|
258
|
+
class Meta:
|
259
|
+
model = User
|
260
|
+
fields = ["email", "is_admin"]
|
261
|
+
|
262
|
+
# Usage
|
263
|
+
form = UserForm(data=request.data)
|
264
|
+
if form.is_valid():
|
265
|
+
user = form.save()
|
266
|
+
```
|
267
|
+
|
268
|
+
## Sharing fields across models
|
269
|
+
|
270
|
+
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.
|
271
|
+
|
272
|
+
```python
|
273
|
+
from plain import models
|
274
|
+
|
275
|
+
|
276
|
+
# Regular Python class for shared fields
|
277
|
+
class TimestampedMixin:
|
278
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
279
|
+
updated_at = models.DateTimeField(auto_now=True)
|
280
|
+
|
281
|
+
|
282
|
+
# Models inherit from the mixin AND models.Model
|
283
|
+
@models.register_model
|
284
|
+
class User(TimestampedMixin, models.Model):
|
285
|
+
email = models.EmailField()
|
286
|
+
password = PasswordField()
|
287
|
+
is_admin = models.BooleanField(default=False)
|
288
|
+
|
289
|
+
|
290
|
+
@models.register_model
|
291
|
+
class Note(TimestampedMixin, models.Model):
|
292
|
+
content = models.TextField(max_length=1024)
|
293
|
+
liked = models.BooleanField(default=False)
|
294
|
+
```
|
295
|
+
|
296
|
+
## Installation
|
297
|
+
|
298
|
+
Install the `plain.models` package from [PyPI](https://pypi.org/project/plain.models/):
|
299
|
+
|
300
|
+
```bash
|
301
|
+
uv add plain.models
|
302
|
+
```
|
303
|
+
|
304
|
+
Then add to your `INSTALLED_PACKAGES`:
|
305
|
+
|
306
|
+
```python
|
307
|
+
# app/settings.py
|
308
|
+
INSTALLED_PACKAGES = [
|
309
|
+
...
|
310
|
+
"plain.models",
|
311
|
+
]
|
312
|
+
```
|
@@ -1,5 +1,31 @@
|
|
1
1
|
# plain-models changelog
|
2
2
|
|
3
|
+
## [0.40.1](https://github.com/dropseed/plain/releases/plain-models@0.40.1) (2025-09-03)
|
4
|
+
|
5
|
+
### What's changed
|
6
|
+
|
7
|
+
- Internal documentation updates for agent commands ([df3edbf0bd](https://github.com/dropseed/plain/commit/df3edbf0bd))
|
8
|
+
|
9
|
+
### Upgrade instructions
|
10
|
+
|
11
|
+
- No changes required
|
12
|
+
|
13
|
+
## [0.40.0](https://github.com/dropseed/plain/releases/plain-models@0.40.0) (2025-08-05)
|
14
|
+
|
15
|
+
### What's changed
|
16
|
+
|
17
|
+
- Foreign key fields now accept lazy objects (like `SimpleLazyObject` used for `request.user`) by automatically evaluating them ([eb78dcc76d](https://github.com/dropseed/plain/commit/eb78dcc76d))
|
18
|
+
- Added `--no-input` option to `plain migrate` command to skip user prompts ([0bdaf0409e](https://github.com/dropseed/plain/commit/0bdaf0409e))
|
19
|
+
- Removed the `plain models optimize-migration` command ([6e4131ab29](https://github.com/dropseed/plain/commit/6e4131ab29))
|
20
|
+
- Removed the `--fake-initial` option from `plain migrate` command ([6506a8bfb9](https://github.com/dropseed/plain/commit/6506a8bfb9))
|
21
|
+
- Fixed CLI help text to reference `plain` commands instead of `manage.py` ([8071854d61](https://github.com/dropseed/plain/commit/8071854d61))
|
22
|
+
|
23
|
+
### Upgrade instructions
|
24
|
+
|
25
|
+
- Remove any usage of `plain models optimize-migration` command - it is no longer available
|
26
|
+
- Remove any usage of `--fake-initial` option from `plain migrate` commands - it is no longer supported
|
27
|
+
- It is no longer necessary to do `user=request.user or None`, for example, when setting foreign key fields with a lazy object like `request.user`. These will now be automatically evaluated.
|
28
|
+
|
3
29
|
## [0.39.2](https://github.com/dropseed/plain/releases/plain-models@0.39.2) (2025-07-25)
|
4
30
|
|
5
31
|
### What's changed
|
@@ -0,0 +1,301 @@
|
|
1
|
+
# plain.models
|
2
|
+
|
3
|
+
**Model your data and store it in a database.**
|
4
|
+
|
5
|
+
- [Overview](#overview)
|
6
|
+
- [Database connection](#database-connection)
|
7
|
+
- [Querying](#querying)
|
8
|
+
- [Migrations](#migrations)
|
9
|
+
- [Fields](#fields)
|
10
|
+
- [Validation](#validation)
|
11
|
+
- [Indexes and constraints](#indexes-and-constraints)
|
12
|
+
- [Managers](#managers)
|
13
|
+
- [Forms](#forms)
|
14
|
+
- [Sharing fields across models](#sharing-fields-across-models)
|
15
|
+
- [Installation](#installation)
|
16
|
+
|
17
|
+
## Overview
|
18
|
+
|
19
|
+
```python
|
20
|
+
# app/users/models.py
|
21
|
+
from plain import models
|
22
|
+
from plain.passwords.models import PasswordField
|
23
|
+
|
24
|
+
|
25
|
+
@models.register_model
|
26
|
+
class User(models.Model):
|
27
|
+
email = models.EmailField()
|
28
|
+
password = PasswordField()
|
29
|
+
is_admin = models.BooleanField(default=False)
|
30
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
31
|
+
|
32
|
+
def __str__(self):
|
33
|
+
return self.email
|
34
|
+
```
|
35
|
+
|
36
|
+
Every model automatically includes an `id` field which serves as the primary
|
37
|
+
key. The name `id` is reserved and can't be used for other fields.
|
38
|
+
|
39
|
+
Create, update, and delete instances of your models:
|
40
|
+
|
41
|
+
```python
|
42
|
+
from .models import User
|
43
|
+
|
44
|
+
|
45
|
+
# Create a new user
|
46
|
+
user = User.objects.create(
|
47
|
+
email="test@example.com",
|
48
|
+
password="password",
|
49
|
+
)
|
50
|
+
|
51
|
+
# Update a user
|
52
|
+
user.email = "new@example.com"
|
53
|
+
user.save()
|
54
|
+
|
55
|
+
# Delete a user
|
56
|
+
user.delete()
|
57
|
+
|
58
|
+
# Query for users
|
59
|
+
admin_users = User.objects.filter(is_admin=True)
|
60
|
+
```
|
61
|
+
|
62
|
+
## Database connection
|
63
|
+
|
64
|
+
To connect to a database, you can provide a `DATABASE_URL` environment variable:
|
65
|
+
|
66
|
+
```sh
|
67
|
+
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
|
68
|
+
```
|
69
|
+
|
70
|
+
Or you can manually define the `DATABASE` setting:
|
71
|
+
|
72
|
+
```python
|
73
|
+
# app/settings.py
|
74
|
+
DATABASE = {
|
75
|
+
"ENGINE": "plain.models.backends.postgresql",
|
76
|
+
"NAME": "dbname",
|
77
|
+
"USER": "user",
|
78
|
+
"PASSWORD": "password",
|
79
|
+
"HOST": "localhost",
|
80
|
+
"PORT": "5432",
|
81
|
+
}
|
82
|
+
```
|
83
|
+
|
84
|
+
Multiple backends are supported, including Postgres, MySQL, and SQLite.
|
85
|
+
|
86
|
+
## Querying
|
87
|
+
|
88
|
+
Models come with a powerful query API through their [`Manager`](./manager.py#Manager) interface:
|
89
|
+
|
90
|
+
```python
|
91
|
+
# Get all users
|
92
|
+
all_users = User.objects.all()
|
93
|
+
|
94
|
+
# Filter users
|
95
|
+
admin_users = User.objects.filter(is_admin=True)
|
96
|
+
recent_users = User.objects.filter(created_at__gte=datetime.now() - timedelta(days=7))
|
97
|
+
|
98
|
+
# Get a single user
|
99
|
+
user = User.objects.get(email="test@example.com")
|
100
|
+
|
101
|
+
# Complex queries with Q objects
|
102
|
+
from plain.models import Q
|
103
|
+
users = User.objects.filter(
|
104
|
+
Q(is_admin=True) | Q(email__endswith="@example.com")
|
105
|
+
)
|
106
|
+
|
107
|
+
# Ordering
|
108
|
+
users = User.objects.order_by("-created_at")
|
109
|
+
|
110
|
+
# Limiting results
|
111
|
+
first_10_users = User.objects.all()[:10]
|
112
|
+
```
|
113
|
+
|
114
|
+
For more advanced querying options, see the [`QuerySet`](./query.py#QuerySet) class.
|
115
|
+
|
116
|
+
## Migrations
|
117
|
+
|
118
|
+
Migrations track changes to your models and update the database schema accordingly:
|
119
|
+
|
120
|
+
```bash
|
121
|
+
# Create migrations for model changes
|
122
|
+
plain makemigrations
|
123
|
+
|
124
|
+
# Apply migrations to the database
|
125
|
+
plain migrate
|
126
|
+
|
127
|
+
# See migration status
|
128
|
+
plain models show-migrations
|
129
|
+
```
|
130
|
+
|
131
|
+
Migrations are Python files that describe database schema changes. They're stored in your app's `migrations/` directory.
|
132
|
+
|
133
|
+
## Fields
|
134
|
+
|
135
|
+
Plain provides many field types for different data:
|
136
|
+
|
137
|
+
```python
|
138
|
+
from plain import models
|
139
|
+
|
140
|
+
class Product(models.Model):
|
141
|
+
# Text fields
|
142
|
+
name = models.CharField(max_length=200)
|
143
|
+
description = models.TextField()
|
144
|
+
|
145
|
+
# Numeric fields
|
146
|
+
price = models.DecimalField(max_digits=10, decimal_places=2)
|
147
|
+
quantity = models.IntegerField(default=0)
|
148
|
+
|
149
|
+
# Boolean fields
|
150
|
+
is_active = models.BooleanField(default=True)
|
151
|
+
|
152
|
+
# Date and time fields
|
153
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
154
|
+
updated_at = models.DateTimeField(auto_now=True)
|
155
|
+
|
156
|
+
# Relationships
|
157
|
+
category = models.ForeignKey("Category", on_delete=models.CASCADE)
|
158
|
+
tags = models.ManyToManyField("Tag")
|
159
|
+
```
|
160
|
+
|
161
|
+
Common field types include:
|
162
|
+
|
163
|
+
- [`CharField`](./fields/__init__.py#CharField)
|
164
|
+
- [`TextField`](./fields/__init__.py#TextField)
|
165
|
+
- [`IntegerField`](./fields/__init__.py#IntegerField)
|
166
|
+
- [`DecimalField`](./fields/__init__.py#DecimalField)
|
167
|
+
- [`BooleanField`](./fields/__init__.py#BooleanField)
|
168
|
+
- [`DateTimeField`](./fields/__init__.py#DateTimeField)
|
169
|
+
- [`EmailField`](./fields/__init__.py#EmailField)
|
170
|
+
- [`URLField`](./fields/__init__.py#URLField)
|
171
|
+
- [`UUIDField`](./fields/__init__.py#UUIDField)
|
172
|
+
|
173
|
+
## Validation
|
174
|
+
|
175
|
+
Models can be validated before saving:
|
176
|
+
|
177
|
+
```python
|
178
|
+
class User(models.Model):
|
179
|
+
email = models.EmailField(unique=True)
|
180
|
+
age = models.IntegerField()
|
181
|
+
|
182
|
+
def clean(self):
|
183
|
+
if self.age < 18:
|
184
|
+
raise ValidationError("User must be 18 or older")
|
185
|
+
|
186
|
+
def save(self, *args, **kwargs):
|
187
|
+
self.full_clean() # Runs validation
|
188
|
+
super().save(*args, **kwargs)
|
189
|
+
```
|
190
|
+
|
191
|
+
Field-level validation happens automatically based on field types and constraints.
|
192
|
+
|
193
|
+
## Indexes and constraints
|
194
|
+
|
195
|
+
Optimize queries and ensure data integrity with indexes and constraints:
|
196
|
+
|
197
|
+
```python
|
198
|
+
class User(models.Model):
|
199
|
+
email = models.EmailField()
|
200
|
+
username = models.CharField(max_length=150)
|
201
|
+
age = models.IntegerField()
|
202
|
+
|
203
|
+
class Meta:
|
204
|
+
indexes = [
|
205
|
+
models.Index(fields=["email"]),
|
206
|
+
models.Index(fields=["-created_at"], name="user_created_idx"),
|
207
|
+
]
|
208
|
+
constraints = [
|
209
|
+
models.UniqueConstraint(fields=["email", "username"], name="unique_user"),
|
210
|
+
models.CheckConstraint(check=models.Q(age__gte=0), name="age_positive"),
|
211
|
+
]
|
212
|
+
```
|
213
|
+
|
214
|
+
## Managers
|
215
|
+
|
216
|
+
[`Manager`](./manager.py#Manager) objects provide the interface for querying models:
|
217
|
+
|
218
|
+
```python
|
219
|
+
class PublishedManager(models.Manager):
|
220
|
+
def get_queryset(self):
|
221
|
+
return super().get_queryset().filter(status="published")
|
222
|
+
|
223
|
+
class Article(models.Model):
|
224
|
+
title = models.CharField(max_length=200)
|
225
|
+
status = models.CharField(max_length=20)
|
226
|
+
|
227
|
+
# Default manager
|
228
|
+
objects = models.Manager()
|
229
|
+
|
230
|
+
# Custom manager
|
231
|
+
published = PublishedManager()
|
232
|
+
|
233
|
+
# Usage
|
234
|
+
all_articles = Article.objects.all()
|
235
|
+
published_articles = Article.published.all()
|
236
|
+
```
|
237
|
+
|
238
|
+
## Forms
|
239
|
+
|
240
|
+
Models integrate with Plain's form system:
|
241
|
+
|
242
|
+
```python
|
243
|
+
from plain import forms
|
244
|
+
from .models import User
|
245
|
+
|
246
|
+
class UserForm(forms.ModelForm):
|
247
|
+
class Meta:
|
248
|
+
model = User
|
249
|
+
fields = ["email", "is_admin"]
|
250
|
+
|
251
|
+
# Usage
|
252
|
+
form = UserForm(data=request.data)
|
253
|
+
if form.is_valid():
|
254
|
+
user = form.save()
|
255
|
+
```
|
256
|
+
|
257
|
+
## Sharing fields across models
|
258
|
+
|
259
|
+
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.
|
260
|
+
|
261
|
+
```python
|
262
|
+
from plain import models
|
263
|
+
|
264
|
+
|
265
|
+
# Regular Python class for shared fields
|
266
|
+
class TimestampedMixin:
|
267
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
268
|
+
updated_at = models.DateTimeField(auto_now=True)
|
269
|
+
|
270
|
+
|
271
|
+
# Models inherit from the mixin AND models.Model
|
272
|
+
@models.register_model
|
273
|
+
class User(TimestampedMixin, models.Model):
|
274
|
+
email = models.EmailField()
|
275
|
+
password = PasswordField()
|
276
|
+
is_admin = models.BooleanField(default=False)
|
277
|
+
|
278
|
+
|
279
|
+
@models.register_model
|
280
|
+
class Note(TimestampedMixin, models.Model):
|
281
|
+
content = models.TextField(max_length=1024)
|
282
|
+
liked = models.BooleanField(default=False)
|
283
|
+
```
|
284
|
+
|
285
|
+
## Installation
|
286
|
+
|
287
|
+
Install the `plain.models` package from [PyPI](https://pypi.org/project/plain.models/):
|
288
|
+
|
289
|
+
```bash
|
290
|
+
uv add plain.models
|
291
|
+
```
|
292
|
+
|
293
|
+
Then add to your `INSTALLED_PACKAGES`:
|
294
|
+
|
295
|
+
```python
|
296
|
+
# app/settings.py
|
297
|
+
INSTALLED_PACKAGES = [
|
298
|
+
...
|
299
|
+
"plain.models",
|
300
|
+
]
|
301
|
+
```
|
@@ -53,11 +53,11 @@ class BaseDatabaseCreation:
|
|
53
53
|
package_label=None,
|
54
54
|
migration_name=None,
|
55
55
|
fake=False,
|
56
|
-
fake_initial=False,
|
57
56
|
plan=False,
|
58
57
|
check_unapplied=False,
|
59
58
|
backup=False,
|
60
59
|
prune=False,
|
60
|
+
no_input=True,
|
61
61
|
verbosity=max(verbosity - 1, 0),
|
62
62
|
)
|
63
63
|
|