plain.models
Model your data and store it in a database.
# app/users/models.py
from plain import models
from plain.passwords.models import PasswordField
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
Create, update, and delete instances of your models:
from .models import User
# Create a new user
user = User.objects.create(
email="[email protected]",
password="password",
)
# Update a user
user.email = "[email protected]"
user.save()
# Delete a user
user.delete()
# Query for users
admin_users = User.objects.filter(is_admin=True)
Installation
# app/settings.py
INSTALLED_PACKAGES = [
...
"plain.models",
]
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 DATABASES
setting.
# app/settings.py
DATABASES = {
"default": {
"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
Migrations
Fields
Validation
Indexes and constraints
Managers
Forms
1import inspect
2from collections import defaultdict
3from itertools import chain
4
5from plain.models.registry import models_registry
6from plain.packages import packages_registry
7from plain.preflight import Error, Warning, register_check
8from plain.runtime import settings
9
10
11@register_check
12def check_database_backends(databases=None, **kwargs):
13 if databases is None:
14 return []
15
16 from plain.models.db import connections
17
18 issues = []
19 for alias in databases:
20 conn = connections[alias]
21 issues.extend(conn.validation.check(**kwargs))
22 return issues
23
24
25@register_check
26def check_all_models(package_configs=None, **kwargs):
27 db_table_models = defaultdict(list)
28 indexes = defaultdict(list)
29 constraints = defaultdict(list)
30 errors = []
31 if package_configs is None:
32 models = models_registry.get_models()
33 else:
34 models = chain.from_iterable(
35 models_registry.get_models(package_label=package_config.label)
36 for package_config in package_configs
37 )
38 for model in models:
39 db_table_models[model._meta.db_table].append(model._meta.label)
40 if not inspect.ismethod(model.check):
41 errors.append(
42 Error(
43 f"The '{model.__name__}.check()' class method is currently overridden by {model.check!r}.",
44 obj=model,
45 id="models.E020",
46 )
47 )
48 else:
49 errors.extend(model.check(**kwargs))
50 for model_index in model._meta.indexes:
51 indexes[model_index.name].append(model._meta.label)
52 for model_constraint in model._meta.constraints:
53 constraints[model_constraint.name].append(model._meta.label)
54 if settings.DATABASE_ROUTERS:
55 error_class, error_id = Warning, "models.W035"
56 error_hint = (
57 "You have configured settings.DATABASE_ROUTERS. Verify that %s "
58 "are correctly routed to separate databases."
59 )
60 else:
61 error_class, error_id = Error, "models.E028"
62 error_hint = None
63 for db_table, model_labels in db_table_models.items():
64 if len(model_labels) != 1:
65 model_labels_str = ", ".join(model_labels)
66 errors.append(
67 error_class(
68 f"db_table '{db_table}' is used by multiple models: {model_labels_str}.",
69 obj=db_table,
70 hint=(error_hint % model_labels_str) if error_hint else None,
71 id=error_id,
72 )
73 )
74 for index_name, model_labels in indexes.items():
75 if len(model_labels) > 1:
76 model_labels = set(model_labels)
77 errors.append(
78 Error(
79 "index name '{}' is not unique {} {}.".format(
80 index_name,
81 "for model" if len(model_labels) == 1 else "among models:",
82 ", ".join(sorted(model_labels)),
83 ),
84 id="models.E029" if len(model_labels) == 1 else "models.E030",
85 ),
86 )
87 for constraint_name, model_labels in constraints.items():
88 if len(model_labels) > 1:
89 model_labels = set(model_labels)
90 errors.append(
91 Error(
92 "constraint name '{}' is not unique {} {}.".format(
93 constraint_name,
94 "for model" if len(model_labels) == 1 else "among models:",
95 ", ".join(sorted(model_labels)),
96 ),
97 id="models.E031" if len(model_labels) == 1 else "models.E032",
98 ),
99 )
100 return errors
101
102
103def _check_lazy_references(models_registry, packages_registry):
104 """
105 Ensure all lazy (i.e. string) model references have been resolved.
106
107 Lazy references are used in various places throughout Plain, primarily in
108 related fields and model signals. Identify those common cases and provide
109 more helpful error messages for them.
110 """
111 pending_models = set(models_registry._pending_operations)
112
113 # Short circuit if there aren't any errors.
114 if not pending_models:
115 return []
116
117 def extract_operation(obj):
118 """
119 Take a callable found in Packages._pending_operations and identify the
120 original callable passed to Packages.lazy_model_operation(). If that
121 callable was a partial, return the inner, non-partial function and
122 any arguments and keyword arguments that were supplied with it.
123
124 obj is a callback defined locally in Packages.lazy_model_operation() and
125 annotated there with a `func` attribute so as to imitate a partial.
126 """
127 operation, args, keywords = obj, [], {}
128 while hasattr(operation, "func"):
129 args.extend(getattr(operation, "args", []))
130 keywords.update(getattr(operation, "keywords", {}))
131 operation = operation.func
132 return operation, args, keywords
133
134 def app_model_error(model_key):
135 try:
136 packages_registry.get_package_config(model_key[0])
137 model_error = "app '{}' doesn't provide model '{}'".format(*model_key)
138 except LookupError:
139 model_error = f"app '{model_key[0]}' isn't installed"
140 return model_error
141
142 # Here are several functions which return CheckMessage instances for the
143 # most common usages of lazy operations throughout Plain. These functions
144 # take the model that was being waited on as an (package_label, modelname)
145 # pair, the original lazy function, and its positional and keyword args as
146 # determined by extract_operation().
147
148 def field_error(model_key, func, args, keywords):
149 error_msg = (
150 "The field %(field)s was declared with a lazy reference "
151 "to '%(model)s', but %(model_error)s."
152 )
153 params = {
154 "model": ".".join(model_key),
155 "field": keywords["field"],
156 "model_error": app_model_error(model_key),
157 }
158 return Error(error_msg % params, obj=keywords["field"], id="fields.E307")
159
160 def default_error(model_key, func, args, keywords):
161 error_msg = (
162 "%(op)s contains a lazy reference to %(model)s, but %(model_error)s."
163 )
164 params = {
165 "op": func,
166 "model": ".".join(model_key),
167 "model_error": app_model_error(model_key),
168 }
169 return Error(error_msg % params, obj=func, id="models.E022")
170
171 # Maps common uses of lazy operations to corresponding error functions
172 # defined above. If a key maps to None, no error will be produced.
173 # default_error() will be used for usages that don't appear in this dict.
174 known_lazy = {
175 ("plain.models.fields.related", "resolve_related_class"): field_error,
176 }
177
178 def build_error(model_key, func, args, keywords):
179 key = (func.__module__, func.__name__)
180 error_fn = known_lazy.get(key, default_error)
181 return error_fn(model_key, func, args, keywords) if error_fn else None
182
183 return sorted(
184 filter(
185 None,
186 (
187 build_error(model_key, *extract_operation(func))
188 for model_key in pending_models
189 for func in models_registry._pending_operations[model_key]
190 ),
191 ),
192 key=lambda error: error.msg,
193 )
194
195
196@register_check
197def check_lazy_references(package_configs=None, **kwargs):
198 return _check_lazy_references(models_registry, packages_registry)
199
200
201@register_check
202def check_database_tables(package_configs, **kwargs):
203 from plain.models.db import connection
204
205 databases = kwargs.get("databases", None)
206 if not databases:
207 return []
208
209 errors = []
210
211 for database in databases:
212 db_tables = connection.introspection.table_names()
213 model_tables = connection.introspection.plain_table_names()
214
215 unknown_tables = set(db_tables) - set(model_tables)
216 unknown_tables.discard("plainmigrations") # Know this could be there
217 if unknown_tables:
218 table_names = ", ".join(unknown_tables)
219 specific_hint = f'echo "DROP TABLE IF EXISTS {unknown_tables.pop()}" | plain models db-shell'
220 errors.append(
221 Warning(
222 f"Unknown tables in {database} database: {table_names}",
223 hint=(
224 "Tables may be from packages/models that have been uninstalled. "
225 "Make sure you have a backup and delete the tables manually "
226 f"(ex. `{specific_hint}`)."
227 ),
228 id="plain.models.W001",
229 )
230 )
231
232 return errors