Plain is headed towards 1.0! Subscribe for development updates →

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

Migration docs

Fields

Field docs

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