diff --git a/docs/data_types.rst b/docs/data_types.rst index 87fc895d..5295bb86 100644 --- a/docs/data_types.rst +++ b/docs/data_types.rst @@ -70,6 +70,14 @@ EncryptedType .. autoclass:: EncryptedType + +GenderType +---------- + +.. module:: sqlalchemy_utils.types.gender + +.. autoclass:: GenderType + JSONType -------- diff --git a/sqlalchemy_utils/__init__.py b/sqlalchemy_utils/__init__.py index d406431f..6967f7ba 100644 --- a/sqlalchemy_utils/__init__.py +++ b/sqlalchemy_utils/__init__.py @@ -71,6 +71,7 @@ DateTimeRangeType, EmailType, EncryptedType, + GenderType, instrumented_list, InstrumentedList, IntRangeType, diff --git a/sqlalchemy_utils/types/__init__.py b/sqlalchemy_utils/types/__init__.py index 272b0017..93ca8f6d 100644 --- a/sqlalchemy_utils/types/__init__.py +++ b/sqlalchemy_utils/types/__init__.py @@ -9,6 +9,7 @@ from .currency import CurrencyType # noqa from .email import EmailType # noqa from .encrypted import EncryptedType # noqa +from .gender import GenderType # noqa from .ip_address import IPAddressType # noqa from .json import JSONType # noqa from .locale import LocaleType # noqa diff --git a/sqlalchemy_utils/types/gender.py b/sqlalchemy_utils/types/gender.py new file mode 100644 index 00000000..f5ce4084 --- /dev/null +++ b/sqlalchemy_utils/types/gender.py @@ -0,0 +1,28 @@ +import sqlalchemy as sa + +from ..operators import CaseInsensitiveComparator + + +class GenderType(sa.types.TypeDecorator): + """ + GenderType represents the gender identity of a person. It is as inclusive + as possible, suggesting "simple" genders like "male" and "female" + but also supporting arbitrary text input for non-binary gender identities. + This data is stored in the database as a lowercase string. + + This database type is primarily useful so that form libraries like wtforms + can detect that this column represents a person's gender, which allows + the form library to render an inclusive HTML form field that allows the + user to select their gender. + """ + impl = sa.Unicode(255) + comparator_factory = CaseInsensitiveComparator + + def process_bind_param(self, value, dialect): + if value is not None: + return value.lower() + return value + + @property + def python_type(self): + return self.impl.type.python_type diff --git a/tests/types/test_gender.py b/tests/types/test_gender.py new file mode 100644 index 00000000..55efaeb6 --- /dev/null +++ b/tests/types/test_gender.py @@ -0,0 +1,41 @@ +import pytest +import sqlalchemy as sa + +from sqlalchemy_utils import GenderType + + +@pytest.fixture +def User(Base): + class User(Base): + __tablename__ = 'user' + id = sa.Column(sa.Integer, primary_key=True) + gender = sa.Column(GenderType) + + def __repr__(self): + return 'User(%r)' % self.id + return User + + +class TestGenderType(object): + def test_saves_gender_as_lowercased(self, session, User): + user = User(gender=u'Male') + + session.add(user) + session.commit() + + user = session.query(User).first() + assert user.gender == u'male' + + def test_literal_param(self, session, User): + clause = User.gender == 'Female' + compiled = str(clause.compile(compile_kwargs={'literal_binds': True})) + assert compiled == '"user".gender = lower(\'Female\')' + + def test_nonbinary_gender(self, session, User): + user = User(gender=u'non-binary') + + session.add(user) + session.commit() + + user = session.query(User).first() + assert user.gender == u'non-binary'