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()
    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

Migration docs

Fields

Field docs

Validation

Indexes and constraints

Managers

Forms

  1import bisect
  2import copy
  3import inspect
  4from collections import defaultdict
  5
  6from plain.exceptions import FieldDoesNotExist
  7from plain.models import models_registry
  8from plain.models.constraints import UniqueConstraint
  9from plain.models.db import connections
 10from plain.models.fields import BigAutoField
 11from plain.models.manager import Manager
 12from plain.utils.datastructures import ImmutableList
 13from plain.utils.functional import cached_property
 14
 15PROXY_PARENTS = object()
 16
 17EMPTY_RELATION_TREE = ()
 18
 19IMMUTABLE_WARNING = (
 20    "The return type of '%s' should never be mutated. If you want to manipulate this "
 21    "list for your own use, make a copy first."
 22)
 23
 24DEFAULT_NAMES = (
 25    "db_table",
 26    "db_table_comment",
 27    "ordering",
 28    "get_latest_by",
 29    "package_label",
 30    "models_registry",
 31    "default_related_name",
 32    "required_db_features",
 33    "required_db_vendor",
 34    "base_manager_name",
 35    "default_manager_name",
 36    "indexes",
 37    "constraints",
 38)
 39
 40
 41def make_immutable_fields_list(name, data):
 42    return ImmutableList(data, warning=IMMUTABLE_WARNING % name)
 43
 44
 45class Options:
 46    FORWARD_PROPERTIES = {
 47        "fields",
 48        "many_to_many",
 49        "concrete_fields",
 50        "local_concrete_fields",
 51        "_non_pk_concrete_field_names",
 52        "_forward_fields_map",
 53        "managers",
 54        "managers_map",
 55        "base_manager",
 56        "default_manager",
 57    }
 58    REVERSE_PROPERTIES = {"related_objects", "fields_map", "_relation_tree"}
 59
 60    default_models_registry = models_registry
 61
 62    def __init__(self, meta, package_label=None):
 63        self._get_fields_cache = {}
 64        self.local_fields = []
 65        self.local_many_to_many = []
 66        self.local_managers = []
 67        self.base_manager_name = None
 68        self.default_manager_name = None
 69        self.model_name = None
 70        self.db_table = ""
 71        self.db_table_comment = ""
 72        self.ordering = []
 73        self.indexes = []
 74        self.constraints = []
 75        self.object_name = None
 76        self.package_label = package_label
 77        self.get_latest_by = None
 78        self.required_db_features = []
 79        self.required_db_vendor = None
 80        self.meta = meta
 81        self.pk = None
 82        self.auto_field = None
 83
 84        # For any non-abstract class, the concrete class is the model
 85        # in the end of the proxy_for_model chain. In particular, for
 86        # concrete models, the concrete_model is always the class itself.
 87        self.concrete_model = None
 88
 89        # List of all lookups defined in ForeignKey 'limit_choices_to' options
 90        # from *other* models. Needed for some admin checks. Internal use only.
 91        self.related_fkey_lookups = []
 92
 93        # A custom app registry to use, if you're making a separate model set.
 94        self.models_registry = self.default_models_registry
 95
 96        self.default_related_name = None
 97
 98    @property
 99    def label(self):
100        return f"{self.package_label}.{self.object_name}"
101
102    @property
103    def label_lower(self):
104        return f"{self.package_label}.{self.model_name}"
105
106    def contribute_to_class(self, cls, name):
107        from plain.models.backends.utils import truncate_name
108        from plain.models.db import connection
109
110        cls._meta = self
111        self.model = cls
112        # First, construct the default values for these options.
113        self.object_name = cls.__name__
114        self.model_name = self.object_name.lower()
115
116        # Store the original user-defined values for each option,
117        # for use when serializing the model definition
118        self.original_attrs = {}
119
120        # Next, apply any overridden values from 'class Meta'.
121        if self.meta:
122            meta_attrs = self.meta.__dict__.copy()
123            for name in self.meta.__dict__:
124                # Ignore any private attributes that Plain doesn't care about.
125                # NOTE: We can't modify a dictionary's contents while looping
126                # over it, so we loop over the *original* dictionary instead.
127                if name.startswith("_"):
128                    del meta_attrs[name]
129            for attr_name in DEFAULT_NAMES:
130                if attr_name in meta_attrs:
131                    setattr(self, attr_name, meta_attrs.pop(attr_name))
132                    self.original_attrs[attr_name] = getattr(self, attr_name)
133                elif hasattr(self.meta, attr_name):
134                    setattr(self, attr_name, getattr(self.meta, attr_name))
135                    self.original_attrs[attr_name] = getattr(self, attr_name)
136
137            # Package label/class name interpolation for names of constraints and
138            # indexes.
139            for attr_name in {"constraints", "indexes"}:
140                objs = getattr(self, attr_name, [])
141                setattr(self, attr_name, self._format_names_with_class(cls, objs))
142
143            # Any leftover attributes must be invalid.
144            if meta_attrs != {}:
145                raise TypeError(
146                    "'class Meta' got invalid attribute(s): {}".format(
147                        ",".join(meta_attrs)
148                    )
149                )
150
151        del self.meta
152
153        # If the db_table wasn't provided, use the package_label + model_name.
154        if not self.db_table:
155            self.db_table = f"{self.package_label}_{self.model_name}"
156            self.db_table = truncate_name(
157                self.db_table, connection.ops.max_name_length()
158            )
159
160    def _format_names_with_class(self, cls, objs):
161        """Package label/class name interpolation for object names."""
162        new_objs = []
163        for obj in objs:
164            obj = obj.clone()
165            obj.name = obj.name % {
166                "package_label": cls._meta.package_label.lower(),
167                "class": cls.__name__.lower(),
168            }
169            new_objs.append(obj)
170        return new_objs
171
172    def _prepare(self, model):
173        if self.pk is None:
174            auto = BigAutoField(primary_key=True, auto_created=True)
175            model.add_to_class("id", auto)
176
177    def add_manager(self, manager):
178        self.local_managers.append(manager)
179        self._expire_cache()
180
181    def add_field(self, field, private=False):
182        # Insert the given field in the order in which it was created, using
183        # the "creation_counter" attribute of the field.
184        # Move many-to-many related fields from self.fields into
185        # self.many_to_many.
186        if field.is_relation and field.many_to_many:
187            bisect.insort(self.local_many_to_many, field)
188        else:
189            bisect.insort(self.local_fields, field)
190            self.setup_pk(field)
191
192        # If the field being added is a relation to another known field,
193        # expire the cache on this field and the forward cache on the field
194        # being referenced, because there will be new relationships in the
195        # cache. Otherwise, expire the cache of references *to* this field.
196        # The mechanism for getting at the related model is slightly odd -
197        # ideally, we'd just ask for field.related_model. However, related_model
198        # is a cached property, and all the models haven't been loaded yet, so
199        # we need to make sure we don't cache a string reference.
200        if (
201            field.is_relation
202            and hasattr(field.remote_field, "model")
203            and field.remote_field.model
204        ):
205            try:
206                field.remote_field.model._meta._expire_cache(forward=False)
207            except AttributeError:
208                pass
209            self._expire_cache()
210        else:
211            self._expire_cache(reverse=False)
212
213    def setup_pk(self, field):
214        if not self.pk and field.primary_key:
215            self.pk = field
216
217    def __repr__(self):
218        return f"<Options for {self.object_name}>"
219
220    def __str__(self):
221        return self.label_lower
222
223    def can_migrate(self, connection):
224        """
225        Return True if the model can/should be migrated on the `connection`.
226        `connection` can be either a real connection or a connection alias.
227        """
228        if isinstance(connection, str):
229            connection = connections[connection]
230        if self.required_db_vendor:
231            return self.required_db_vendor == connection.vendor
232        if self.required_db_features:
233            return all(
234                getattr(connection.features, feat, False)
235                for feat in self.required_db_features
236            )
237        return True
238
239    @cached_property
240    def managers(self):
241        managers = []
242        seen_managers = set()
243        bases = (b for b in self.model.mro() if hasattr(b, "_meta"))
244        for depth, base in enumerate(bases):
245            for manager in base._meta.local_managers:
246                if manager.name in seen_managers:
247                    continue
248
249                manager = copy.copy(manager)
250                manager.model = self.model
251                seen_managers.add(manager.name)
252                managers.append((depth, manager.creation_counter, manager))
253
254        return make_immutable_fields_list(
255            "managers",
256            (m[2] for m in sorted(managers)),
257        )
258
259    @cached_property
260    def managers_map(self):
261        return {manager.name: manager for manager in self.managers}
262
263    @cached_property
264    def base_manager(self):
265        base_manager_name = self.base_manager_name
266        if not base_manager_name:
267            # Get the first parent's base_manager_name if there's one.
268            for parent in self.model.mro()[1:]:
269                if hasattr(parent, "_meta"):
270                    if parent._base_manager.name != "_base_manager":
271                        base_manager_name = parent._base_manager.name
272                    break
273
274        if base_manager_name:
275            try:
276                return self.managers_map[base_manager_name]
277            except KeyError:
278                raise ValueError(
279                    f"{self.object_name} has no manager named {base_manager_name!r}"
280                )
281
282        manager = Manager()
283        manager.name = "_base_manager"
284        manager.model = self.model
285        manager.auto_created = True
286        return manager
287
288    @cached_property
289    def default_manager(self):
290        default_manager_name = self.default_manager_name
291        if not default_manager_name and not self.local_managers:
292            # Get the first parent's default_manager_name if there's one.
293            for parent in self.model.mro()[1:]:
294                if hasattr(parent, "_meta"):
295                    default_manager_name = parent._meta.default_manager_name
296                    break
297
298        if default_manager_name:
299            try:
300                return self.managers_map[default_manager_name]
301            except KeyError:
302                raise ValueError(
303                    f"{self.object_name} has no manager named {default_manager_name!r}"
304                )
305
306        if self.managers:
307            return self.managers[0]
308
309    @cached_property
310    def fields(self):
311        """
312        Return a list of all forward fields on the model and its parents,
313        excluding ManyToManyFields.
314
315        Private API intended only to be used by Plain itself; get_fields()
316        combined with filtering of field properties is the public API for
317        obtaining this field list.
318        """
319
320        # For legacy reasons, the fields property should only contain forward
321        # fields that are not private or with a m2m cardinality. Therefore we
322        # pass these three filters as filters to the generator.
323        # The third lambda is a longwinded way of checking f.related_model - we don't
324        # use that property directly because related_model is a cached property,
325        # and all the models may not have been loaded yet; we don't want to cache
326        # the string reference to the related_model.
327        def is_not_an_m2m_field(f):
328            return not (f.is_relation and f.many_to_many)
329
330        def is_not_a_generic_relation(f):
331            return not (f.is_relation and f.one_to_many)
332
333        def is_not_a_generic_foreign_key(f):
334            return not (
335                f.is_relation
336                and f.many_to_one
337                and not (hasattr(f.remote_field, "model") and f.remote_field.model)
338            )
339
340        return make_immutable_fields_list(
341            "fields",
342            (
343                f
344                for f in self._get_fields(reverse=False)
345                if is_not_an_m2m_field(f)
346                and is_not_a_generic_relation(f)
347                and is_not_a_generic_foreign_key(f)
348            ),
349        )
350
351    @cached_property
352    def concrete_fields(self):
353        """
354        Return a list of all concrete fields on the model and its parents.
355
356        Private API intended only to be used by Plain itself; get_fields()
357        combined with filtering of field properties is the public API for
358        obtaining this field list.
359        """
360        return make_immutable_fields_list(
361            "concrete_fields", (f for f in self.fields if f.concrete)
362        )
363
364    @cached_property
365    def local_concrete_fields(self):
366        """
367        Return a list of all concrete fields on the model.
368
369        Private API intended only to be used by Plain itself; get_fields()
370        combined with filtering of field properties is the public API for
371        obtaining this field list.
372        """
373        return make_immutable_fields_list(
374            "local_concrete_fields", (f for f in self.local_fields if f.concrete)
375        )
376
377    @cached_property
378    def many_to_many(self):
379        """
380        Return a list of all many to many fields on the model and its parents.
381
382        Private API intended only to be used by Plain itself; get_fields()
383        combined with filtering of field properties is the public API for
384        obtaining this list.
385        """
386        return make_immutable_fields_list(
387            "many_to_many",
388            (
389                f
390                for f in self._get_fields(reverse=False)
391                if f.is_relation and f.many_to_many
392            ),
393        )
394
395    @cached_property
396    def related_objects(self):
397        """
398        Return all related objects pointing to the current model. The related
399        objects can come from a one-to-one, one-to-many, or many-to-many field
400        relation type.
401
402        Private API intended only to be used by Plain itself; get_fields()
403        combined with filtering of field properties is the public API for
404        obtaining this field list.
405        """
406        all_related_fields = self._get_fields(
407            forward=False, reverse=True, include_hidden=True
408        )
409        return make_immutable_fields_list(
410            "related_objects",
411            (
412                obj
413                for obj in all_related_fields
414                if not obj.hidden or obj.field.many_to_many
415            ),
416        )
417
418    @cached_property
419    def _forward_fields_map(self):
420        res = {}
421        fields = self._get_fields(reverse=False)
422        for field in fields:
423            res[field.name] = field
424            # Due to the way Plain's internals work, get_field() should also
425            # be able to fetch a field by attname. In the case of a concrete
426            # field with relation, includes the *_id name too
427            try:
428                res[field.attname] = field
429            except AttributeError:
430                pass
431        return res
432
433    @cached_property
434    def fields_map(self):
435        res = {}
436        fields = self._get_fields(forward=False, include_hidden=True)
437        for field in fields:
438            res[field.name] = field
439            # Due to the way Plain's internals work, get_field() should also
440            # be able to fetch a field by attname. In the case of a concrete
441            # field with relation, includes the *_id name too
442            try:
443                res[field.attname] = field
444            except AttributeError:
445                pass
446        return res
447
448    def get_field(self, field_name):
449        """
450        Return a field instance given the name of a forward or reverse field.
451        """
452        try:
453            # In order to avoid premature loading of the relation tree
454            # (expensive) we prefer checking if the field is a forward field.
455            return self._forward_fields_map[field_name]
456        except KeyError:
457            # If the app registry is not ready, reverse fields are
458            # unavailable, therefore we throw a FieldDoesNotExist exception.
459            if not self.models_registry.ready:
460                raise FieldDoesNotExist(
461                    f"{self.object_name} has no field named '{field_name}'. The app cache isn't ready yet, "
462                    "so if this is an auto-created related field, it won't "
463                    "be available yet."
464                )
465
466        try:
467            # Retrieve field instance by name from cached or just-computed
468            # field map.
469            return self.fields_map[field_name]
470        except KeyError:
471            raise FieldDoesNotExist(
472                f"{self.object_name} has no field named '{field_name}'"
473            )
474
475    def _populate_directed_relation_graph(self):
476        """
477        This method is used by each model to find its reverse objects. As this
478        method is very expensive and is accessed frequently (it looks up every
479        field in a model, in every app), it is computed on first access and then
480        is set as a property on every model.
481        """
482        related_objects_graph = defaultdict(list)
483
484        all_models = self.models_registry.get_models()
485        for model in all_models:
486            opts = model._meta
487
488            fields_with_relations = (
489                f
490                for f in opts._get_fields(reverse=False)
491                if f.is_relation and f.related_model is not None
492            )
493            for f in fields_with_relations:
494                if not isinstance(f.remote_field.model, str):
495                    remote_label = f.remote_field.model._meta.concrete_model._meta.label
496                    related_objects_graph[remote_label].append(f)
497
498        for model in all_models:
499            # Set the relation_tree using the internal __dict__. In this way
500            # we avoid calling the cached property. In attribute lookup,
501            # __dict__ takes precedence over a data descriptor (such as
502            # @cached_property). This means that the _meta._relation_tree is
503            # only called if related_objects is not in __dict__.
504            related_objects = related_objects_graph[
505                model._meta.concrete_model._meta.label
506            ]
507            model._meta.__dict__["_relation_tree"] = related_objects
508        # It seems it is possible that self is not in all_models, so guard
509        # against that with default for get().
510        return self.__dict__.get("_relation_tree", EMPTY_RELATION_TREE)
511
512    @cached_property
513    def _relation_tree(self):
514        return self._populate_directed_relation_graph()
515
516    def _expire_cache(self, forward=True, reverse=True):
517        # This method is usually called by packages.cache_clear(), when the
518        # registry is finalized, or when a new field is added.
519        if forward:
520            for cache_key in self.FORWARD_PROPERTIES:
521                if cache_key in self.__dict__:
522                    delattr(self, cache_key)
523        if reverse:
524            for cache_key in self.REVERSE_PROPERTIES:
525                if cache_key in self.__dict__:
526                    delattr(self, cache_key)
527        self._get_fields_cache = {}
528
529    def get_fields(self, include_hidden=False):
530        """
531        Return a list of fields associated to the model. By default, include
532        forward and reverse fields, fields derived from inheritance, but not
533        hidden fields. The returned fields can be changed using the parameters:
534
535        - include_hidden:  include fields that have a related_name that
536                           starts with a "+"
537        """
538        return self._get_fields(include_hidden=include_hidden)
539
540    def _get_fields(
541        self,
542        forward=True,
543        reverse=True,
544        include_hidden=False,
545        seen_models=None,
546    ):
547        """
548        Internal helper function to return fields of the model.
549        * If forward=True, then fields defined on this model are returned.
550        * If reverse=True, then relations pointing to this model are returned.
551        * If include_hidden=True, then fields with is_hidden=True are returned.
552        """
553
554        # This helper function is used to allow recursion in ``get_fields()``
555        # implementation and to provide a fast way for Plain's internals to
556        # access specific subsets of fields.
557
558        # We must keep track of which models we have already seen. Otherwise we
559        # could include the same field multiple times from different models.
560        topmost_call = seen_models is None
561        if topmost_call:
562            seen_models = set()
563        seen_models.add(self.model)
564
565        # Creates a cache key composed of all arguments
566        cache_key = (forward, reverse, include_hidden, topmost_call)
567
568        try:
569            # In order to avoid list manipulation. Always return a shallow copy
570            # of the results.
571            return self._get_fields_cache[cache_key]
572        except KeyError:
573            pass
574
575        fields = []
576
577        if reverse:
578            # Tree is computed once and cached until the app cache is expired.
579            # It is composed of a list of fields pointing to the current model
580            # from other models.
581            all_fields = self._relation_tree
582            for field in all_fields:
583                # If hidden fields should be included or the relation is not
584                # intentionally hidden, add to the fields dict.
585                if include_hidden or not field.remote_field.hidden:
586                    fields.append(field.remote_field)
587
588        if forward:
589            fields += self.local_fields
590            fields += self.local_many_to_many
591
592        # In order to avoid list manipulation. Always
593        # return a shallow copy of the results
594        fields = make_immutable_fields_list("get_fields()", fields)
595
596        # Store result into cache for later access
597        self._get_fields_cache[cache_key] = fields
598        return fields
599
600    @cached_property
601    def total_unique_constraints(self):
602        """
603        Return a list of total unique constraints. Useful for determining set
604        of fields guaranteed to be unique for all rows.
605        """
606        return [
607            constraint
608            for constraint in self.constraints
609            if (
610                isinstance(constraint, UniqueConstraint)
611                and constraint.condition is None
612                and not constraint.contains_expressions
613            )
614        ]
615
616    @cached_property
617    def _property_names(self):
618        """Return a set of the names of the properties defined on the model."""
619        names = []
620        for name in dir(self.model):
621            attr = inspect.getattr_static(self.model, name)
622            if isinstance(attr, property):
623                names.append(name)
624        return frozenset(names)
625
626    @cached_property
627    def _non_pk_concrete_field_names(self):
628        """
629        Return a set of the non-pk concrete field names defined on the model.
630        """
631        names = []
632        for field in self.concrete_fields:
633            if not field.primary_key:
634                names.append(field.name)
635                if field.name != field.attname:
636                    names.append(field.attname)
637        return frozenset(names)
638
639    @cached_property
640    def db_returning_fields(self):
641        """
642        Private API intended only to be used by Plain itself.
643        Fields to be returned after a database insert.
644        """
645        return [
646            field
647            for field in self._get_fields(forward=True, reverse=False)
648            if getattr(field, "db_returning", False)
649        ]