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(unique=True)
password = PasswordField()
is_staff = 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
staff_users = User.objects.filter(is_staff=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
2import types
3from collections import defaultdict
4from itertools import chain
5
6from plain.packages import packages
7from plain.preflight import Error, Warning, register
8from plain.runtime import settings
9
10
11@register
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
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 = packages.get_models()
33 else:
34 models = chain.from_iterable(
35 package_config.get_models() for package_config in package_configs
36 )
37 for model in models:
38 if model._meta.managed:
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 "The '{}.check()' class method is currently overridden by {!r}.".format(
44 model.__name__, model.check
45 ),
46 obj=model,
47 id="models.E020",
48 )
49 )
50 else:
51 errors.extend(model.check(**kwargs))
52 for model_index in model._meta.indexes:
53 indexes[model_index.name].append(model._meta.label)
54 for model_constraint in model._meta.constraints:
55 constraints[model_constraint.name].append(model._meta.label)
56 if settings.DATABASE_ROUTERS:
57 error_class, error_id = Warning, "models.W035"
58 error_hint = (
59 "You have configured settings.DATABASE_ROUTERS. Verify that %s "
60 "are correctly routed to separate databases."
61 )
62 else:
63 error_class, error_id = Error, "models.E028"
64 error_hint = None
65 for db_table, model_labels in db_table_models.items():
66 if len(model_labels) != 1:
67 model_labels_str = ", ".join(model_labels)
68 errors.append(
69 error_class(
70 "db_table '{}' is used by multiple models: {}.".format(
71 db_table, model_labels_str
72 ),
73 obj=db_table,
74 hint=(error_hint % model_labels_str) if error_hint else None,
75 id=error_id,
76 )
77 )
78 for index_name, model_labels in indexes.items():
79 if len(model_labels) > 1:
80 model_labels = set(model_labels)
81 errors.append(
82 Error(
83 "index name '{}' is not unique {} {}.".format(
84 index_name,
85 "for model" if len(model_labels) == 1 else "among models:",
86 ", ".join(sorted(model_labels)),
87 ),
88 id="models.E029" if len(model_labels) == 1 else "models.E030",
89 ),
90 )
91 for constraint_name, model_labels in constraints.items():
92 if len(model_labels) > 1:
93 model_labels = set(model_labels)
94 errors.append(
95 Error(
96 "constraint name '{}' is not unique {} {}.".format(
97 constraint_name,
98 "for model" if len(model_labels) == 1 else "among models:",
99 ", ".join(sorted(model_labels)),
100 ),
101 id="models.E031" if len(model_labels) == 1 else "models.E032",
102 ),
103 )
104 return errors
105
106
107def _check_lazy_references(packages, ignore=None):
108 """
109 Ensure all lazy (i.e. string) model references have been resolved.
110
111 Lazy references are used in various places throughout Plain, primarily in
112 related fields and model signals. Identify those common cases and provide
113 more helpful error messages for them.
114
115 The ignore parameter is used by StatePackages to exclude swappable models from
116 this check.
117 """
118 pending_models = set(packages._pending_operations) - (ignore or set())
119
120 # Short circuit if there aren't any errors.
121 if not pending_models:
122 return []
123
124 from plain.models import signals
125
126 model_signals = {
127 signal: name
128 for name, signal in vars(signals).items()
129 if isinstance(signal, signals.ModelSignal)
130 }
131
132 def extract_operation(obj):
133 """
134 Take a callable found in Packages._pending_operations and identify the
135 original callable passed to Packages.lazy_model_operation(). If that
136 callable was a partial, return the inner, non-partial function and
137 any arguments and keyword arguments that were supplied with it.
138
139 obj is a callback defined locally in Packages.lazy_model_operation() and
140 annotated there with a `func` attribute so as to imitate a partial.
141 """
142 operation, args, keywords = obj, [], {}
143 while hasattr(operation, "func"):
144 args.extend(getattr(operation, "args", []))
145 keywords.update(getattr(operation, "keywords", {}))
146 operation = operation.func
147 return operation, args, keywords
148
149 def app_model_error(model_key):
150 try:
151 packages.get_package_config(model_key[0])
152 model_error = "app '{}' doesn't provide model '{}'".format(*model_key)
153 except LookupError:
154 model_error = "app '%s' isn't installed" % model_key[0]
155 return model_error
156
157 # Here are several functions which return CheckMessage instances for the
158 # most common usages of lazy operations throughout Plain. These functions
159 # take the model that was being waited on as an (package_label, modelname)
160 # pair, the original lazy function, and its positional and keyword args as
161 # determined by extract_operation().
162
163 def field_error(model_key, func, args, keywords):
164 error_msg = (
165 "The field %(field)s was declared with a lazy reference "
166 "to '%(model)s', but %(model_error)s."
167 )
168 params = {
169 "model": ".".join(model_key),
170 "field": keywords["field"],
171 "model_error": app_model_error(model_key),
172 }
173 return Error(error_msg % params, obj=keywords["field"], id="fields.E307")
174
175 def signal_connect_error(model_key, func, args, keywords):
176 error_msg = (
177 "%(receiver)s was connected to the '%(signal)s' signal with a "
178 "lazy reference to the sender '%(model)s', but %(model_error)s."
179 )
180 receiver = args[0]
181 # The receiver is either a function or an instance of class
182 # defining a `__call__` method.
183 if isinstance(receiver, types.FunctionType):
184 description = "The function '%s'" % receiver.__name__
185 elif isinstance(receiver, types.MethodType):
186 description = "Bound method '{}.{}'".format(
187 receiver.__self__.__class__.__name__,
188 receiver.__name__,
189 )
190 else:
191 description = "An instance of class '%s'" % receiver.__class__.__name__
192 signal_name = model_signals.get(func.__self__, "unknown")
193 params = {
194 "model": ".".join(model_key),
195 "receiver": description,
196 "signal": signal_name,
197 "model_error": app_model_error(model_key),
198 }
199 return Error(error_msg % params, obj=receiver.__module__, id="signals.E001")
200
201 def default_error(model_key, func, args, keywords):
202 error_msg = (
203 "%(op)s contains a lazy reference to %(model)s, but %(model_error)s."
204 )
205 params = {
206 "op": func,
207 "model": ".".join(model_key),
208 "model_error": app_model_error(model_key),
209 }
210 return Error(error_msg % params, obj=func, id="models.E022")
211
212 # Maps common uses of lazy operations to corresponding error functions
213 # defined above. If a key maps to None, no error will be produced.
214 # default_error() will be used for usages that don't appear in this dict.
215 known_lazy = {
216 ("plain.models.fields.related", "resolve_related_class"): field_error,
217 ("plain.models.fields.related", "set_managed"): None,
218 ("plain.signals.dispatch.dispatcher", "connect"): signal_connect_error,
219 }
220
221 def build_error(model_key, func, args, keywords):
222 key = (func.__module__, func.__name__)
223 error_fn = known_lazy.get(key, default_error)
224 return error_fn(model_key, func, args, keywords) if error_fn else None
225
226 return sorted(
227 filter(
228 None,
229 (
230 build_error(model_key, *extract_operation(func))
231 for model_key in pending_models
232 for func in packages._pending_operations[model_key]
233 ),
234 ),
235 key=lambda error: error.msg,
236 )
237
238
239@register
240def check_lazy_references(package_configs=None, **kwargs):
241 return _check_lazy_references(packages)
242
243
244@register
245def check_database_tables(package_configs, **kwargs):
246 from plain.models.db import connection
247
248 databases = kwargs.get("databases", None)
249 if not databases:
250 return []
251
252 errors = []
253
254 for database in databases:
255 db_tables = connection.introspection.table_names()
256 model_tables = connection.introspection.plain_table_names()
257
258 unknown_tables = set(db_tables) - set(model_tables)
259 unknown_tables.discard("plainmigrations") # Know this could be there
260 if unknown_tables:
261 table_names = ", ".join(unknown_tables)
262 specific_hint = f'echo "DROP TABLE IF EXISTS {unknown_tables.pop()}" | plain models db-shell'
263 errors.append(
264 Warning(
265 f"Unknown tables in {database} database: {table_names}",
266 hint=(
267 "Tables may be from packages/models that have been uninstalled. "
268 "Make sure you have a backup and delete the tables manually "
269 f"(ex. `{specific_hint}`)."
270 ),
271 id="plain.models.W001",
272 )
273 )
274
275 return errors