Coverage for .tox/coverage/lib/python3.11/site-packages/wuttjamaican/db/model/base.py: 100%
32 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-15 11:33 -0500
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-15 11:33 -0500
1# -*- coding: utf-8; -*-
2################################################################################
3#
4# WuttJamaican -- Base package for Wutta Framework
5# Copyright © 2023-2024 Lance Edgar
6#
7# This file is part of Wutta Framework.
8#
9# Wutta Framework is free software: you can redistribute it and/or modify it
10# under the terms of the GNU General Public License as published by the Free
11# Software Foundation, either version 3 of the License, or (at your option) any
12# later version.
13#
14# Wutta Framework is distributed in the hope that it will be useful, but
15# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
17# more details.
18#
19# You should have received a copy of the GNU General Public License along with
20# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
21#
22################################################################################
23"""
24Base Models
26.. class:: Base
28 This is the base class for all :term:`data models <data model>` in
29 the :term:`app database`. You should inherit from this class when
30 defining custom models.
32 This class inherits from :class:`WuttaModelBase`.
33"""
35import sqlalchemy as sa
36from sqlalchemy import orm
37from sqlalchemy.ext.associationproxy import association_proxy
39from wuttjamaican.db.util import (naming_convention, ModelBase,
40 uuid_column, uuid_fk_column)
43class WuttaModelBase(ModelBase):
44 """
45 Base class for data models, from which :class:`Base` inherits.
47 Custom models should inherit from :class:`Base` instead of this
48 class.
49 """
51 @classmethod
52 def make_proxy(cls, main_class, extension, name, proxy_name=None):
53 """
54 Convenience method to declare an "association proxy" for the
55 main class, per the params.
57 For more info see
58 :doc:`sqlalchemy:orm/extensions/associationproxy`.
60 :param main_class: Reference to the "parent" model class, upon
61 which the proxy will be defined.
63 :param extension: Attribute name on the main class, which
64 references the extension record.
66 :param name: Attribute name on the extension class, which
67 provides the proxied value.
69 :param proxy_name: Optional attribute name on the main class,
70 which will reference the proxy. If not specified, ``name``
71 will be used.
73 As a simple example consider this model, which extends the
74 :class:`~wuttjamaican.db.model.auth.User` class. In
75 particular note the last line which is what we're documenting
76 here::
78 import sqlalchemy as sa
79 from sqlalchemy import orm
80 from wuttjamaican.db import model
82 class PoserUser(model.Base):
83 \""" Poser extension for User \"""
84 __tablename__ = 'poser_user'
86 uuid = model.uuid_column(sa.ForeignKey('user.uuid'), default=None)
87 user = orm.relationship(
88 model.User,
89 doc="Reference to the main User record.",
90 backref=orm.backref(
91 '_poser',
92 uselist=False,
93 cascade='all, delete-orphan',
94 doc="Reference to the Poser extension record."))
96 favorite_color = sa.Column(sa.String(length=100), nullable=False, doc=\"""
97 User's favorite color.
98 \""")
100 def __str__(self):
101 return str(self.user)
103 # nb. this is the method call
104 PoserUser.make_proxy(model.User, '_poser', 'favorite_color')
106 That code defines a ``PoserUser`` model but also defines a
107 ``favorite_color`` attribute on the main ``User`` class, such
108 that it can be used normally::
110 user = model.User(username='barney', favorite_color='green')
111 session.add(user)
113 user = session.query(model.User).filter_by(username='bambam').one()
114 print(user.favorite_color)
115 """
116 proxy = association_proxy(
117 extension, proxy_name or name,
118 creator=lambda value: cls(**{name: value}))
119 setattr(main_class, name, proxy)
122metadata = sa.MetaData(naming_convention=naming_convention)
124Base = orm.declarative_base(metadata=metadata, cls=WuttaModelBase)
127class Setting(Base):
128 """
129 Represents a :term:`config setting`.
130 """
131 __tablename__ = 'setting'
133 name = sa.Column(sa.String(length=255), primary_key=True, nullable=False, doc="""
134 Unique name for the setting.
135 """)
137 value = sa.Column(sa.Text(), nullable=True, doc="""
138 String value for the setting.
139 """)
141 def __str__(self):
142 return self.name or ""
145class Person(Base):
146 """
147 Represents a person.
149 The use for this table in the base framework, is to associate with
150 a :class:`~wuttjamaican.db.model.auth.User` to provide first and
151 last name etc. (However a user does not have to be associated
152 with any person.)
154 But this table could also be used as a basis for a Customer or
155 Employee relationship etc.
156 """
157 __tablename__ = 'person'
158 __versioned__ = {}
160 uuid = uuid_column()
162 full_name = sa.Column(sa.String(length=100), nullable=False, doc="""
163 Full name for the person. Note that this is *required*.
164 """)
166 first_name = sa.Column(sa.String(length=50), nullable=True, doc="""
167 The person's first name.
168 """)
170 middle_name = sa.Column(sa.String(length=50), nullable=True, doc="""
171 The person's middle name or initial.
172 """)
174 last_name = sa.Column(sa.String(length=50), nullable=True, doc="""
175 The person's last name.
176 """)
178 users = orm.relationship(
179 'User',
180 back_populates='person',
181 cascade_backrefs=False,
182 doc="""
183 List of :class:`~wuttjamaican.db.model.auth.User` accounts for
184 the person. Typically there is only one user account per
185 person, but technically multiple are supported.
186 """)
188 def __str__(self):
189 return self.full_name or ""
191 @property
192 def user(self):
193 """
194 Reference to the "first"
195 :class:`~wuttjamaican.db.model.auth.User` account for the
196 person, or ``None``.
198 .. warning::
200 Note that the database schema supports multiple users per
201 person, but this property logic ignores that and will only
202 ever return "one or none". That might be fine in 99% of
203 cases, but if multiple accounts exist for a person, the one
204 returned is indeterminate.
206 See :attr:`users` to access the full list.
207 """
209 # TODO: i'm not crazy about the ambiguity here re: number of
210 # user accounts a person may have. in particular it's not
211 # clear *which* user account would be returned, as there is no
212 # sequence ordinal defined etc. a better approach might be to
213 # force callers to assume the possibility of multiple
214 # user accounts per person? (if so, remove this property)
216 if self.users:
217 return self.users[0]