diff --git a/django/db/models/functions/__init__.py b/django/db/models/functions/__init__.py index 2dfb62769f3..5528be8c485 100644 --- a/django/db/models/functions/__init__.py +++ b/django/db/models/functions/__init__.py @@ -6,8 +6,8 @@ from .datetime import ( TruncQuarter, TruncSecond, TruncTime, TruncWeek, TruncYear, ) from .text import ( - Chr, Concat, ConcatPair, Left, Length, Lower, Ord, Replace, Right, - StrIndex, Substr, Upper, + Chr, Concat, ConcatPair, Left, Length, Lower, LTrim, Ord, Replace, Right, + RTrim, StrIndex, Substr, Trim, Upper, ) from .window import ( CumeDist, DenseRank, FirstValue, Lag, LastValue, Lead, NthValue, Ntile, @@ -24,8 +24,8 @@ __all__ = [ 'TruncMinute', 'TruncMonth', 'TruncQuarter', 'TruncSecond', 'TruncTime', 'TruncWeek', 'TruncYear', # text - 'Chr', 'Concat', 'ConcatPair', 'Left', 'Length', 'Lower', 'Ord', 'Replace', - 'Right', 'StrIndex', 'Substr', 'Upper', + 'Chr', 'Concat', 'ConcatPair', 'Left', 'Length', 'Lower', 'LTrim', 'Ord', + 'Replace', 'Right', 'RTrim', 'StrIndex', 'Substr', 'Trim', 'Upper', # window 'CumeDist', 'DenseRank', 'FirstValue', 'Lag', 'LastValue', 'Lead', 'NthValue', 'Ntile', 'PercentRank', 'Rank', 'RowNumber', diff --git a/django/db/models/functions/text.py b/django/db/models/functions/text.py index d22928343cb..614522017f7 100644 --- a/django/db/models/functions/text.py +++ b/django/db/models/functions/text.py @@ -110,6 +110,11 @@ class Lower(Transform): lookup_name = 'lower' +class LTrim(Transform): + function = 'LTRIM' + lookup_name = 'ltrim' + + class Ord(Transform): function = 'ASCII' lookup_name = 'ord' @@ -136,6 +141,11 @@ class Right(Left): return Substr(self.source_expressions[0], self.source_expressions[1] * Value(-1)) +class RTrim(Transform): + function = 'RTRIM' + lookup_name = 'rtrim' + + class StrIndex(Func): """ Return a positive integer corresponding to the 1-indexed position of the @@ -174,6 +184,11 @@ class Substr(Func): return super().as_sql(compiler, connection, function='SUBSTR') +class Trim(Transform): + function = 'TRIM' + lookup_name = 'trim' + + class Upper(Transform): function = 'UPPER' lookup_name = 'upper' diff --git a/docs/ref/models/database-functions.txt b/docs/ref/models/database-functions.txt index 0030de55b93..9208a905744 100644 --- a/docs/ref/models/database-functions.txt +++ b/docs/ref/models/database-functions.txt @@ -800,6 +800,16 @@ Usage example:: >>> print(author.name_lower) margaret smith +``LTrim`` +--------- + +.. class:: LTrim(expression, **extra) + +.. versionadded:: 2.1 + +Similar to :class:`~django.db.models.functions.Trim`, but removes only leading +spaces. + ``Ord`` ------- @@ -862,6 +872,16 @@ Usage example:: >>> print(author.last_letter) h +``RTrim`` +--------- + +.. class:: RTrim(expression, **extra) + +.. versionadded:: 2.1 + +Similar to :class:`~django.db.models.functions.Trim`, but removes only trailing +spaces. + ``StrIndex`` ------------ @@ -915,6 +935,25 @@ Usage example:: >>> print(Author.objects.get(name='Margaret Smith').alias) marga +``Trim`` +-------- + +.. class:: Trim(expression, **extra) + +.. versionadded:: 2.1 + +Returns the value of the given text field or expression with leading and +trailing spaces removed. + +Usage example:: + + >>> from django.db.models.functions import Trim + >>> Author.objects.create(name=' John ', alias='j') + >>> Author.objects.update(name=Trim('name')) + 1 + >>> print(Author.objects.get(alias='j').name) + John + ``Upper`` --------- diff --git a/docs/releases/2.1.txt b/docs/releases/2.1.txt index aa4c6b14a47..35261b28501 100644 --- a/docs/releases/2.1.txt +++ b/docs/releases/2.1.txt @@ -204,10 +204,13 @@ Models * A number of new text database functions are added: :class:`~django.db.models.functions.Chr`, - :class:`~django.db.models.functions.Ord`, :class:`~django.db.models.functions.Left`, - :class:`~django.db.models.functions.Right`, and - :class:`~django.db.models.functions.Replace`. + :class:`~django.db.models.functions.LTrim`, + :class:`~django.db.models.functions.Ord`, + :class:`~django.db.models.functions.Replace`, + :class:`~django.db.models.functions.Right`, + :class:`~django.db.models.functions.RTrim`, and + :class:`~django.db.models.functions.Trim`. * The new :class:`~django.db.models.functions.TruncWeek` function truncates :class:`~django.db.models.DateField` and diff --git a/tests/db_functions/test_trim.py b/tests/db_functions/test_trim.py new file mode 100644 index 00000000000..687d1522d31 --- /dev/null +++ b/tests/db_functions/test_trim.py @@ -0,0 +1,40 @@ +from django.db.models import CharField +from django.db.models.functions import LTrim, RTrim, Trim +from django.test import TestCase + +from .models import Author + + +class TrimTests(TestCase): + def test_trim(self): + Author.objects.create(name=' John ', alias='j') + Author.objects.create(name='Rhonda', alias='r') + authors = Author.objects.annotate( + ltrim=LTrim('name'), + rtrim=RTrim('name'), + trim=Trim('name'), + ) + self.assertQuerysetEqual( + authors.order_by('alias'), [ + ('John ', ' John', 'John'), + ('Rhonda', 'Rhonda', 'Rhonda'), + ], + lambda a: (a.ltrim, a.rtrim, a.trim) + ) + + def test_trim_transform(self): + Author.objects.create(name=' John ') + Author.objects.create(name='Rhonda') + tests = ( + (LTrim, 'John '), + (RTrim, ' John'), + (Trim, 'John'), + ) + for transform, trimmed_name in tests: + with self.subTest(transform=transform): + try: + CharField.register_lookup(transform) + authors = Author.objects.filter(**{'name__%s' % transform.lookup_name: trimmed_name}) + self.assertQuerysetEqual(authors, [' John '], lambda a: a.name) + finally: + CharField._unregister_lookup(transform)