From fcb5dbfec0542faaa1b0adad754a1caf1bcd65e2 Mon Sep 17 00:00:00 2001
From: Paolo Melchiorre <paolo@melchiorre.org>
Date: Wed, 29 Mar 2017 23:52:42 +0200
Subject: [PATCH] Fixed #27996 -- Added RandomUUID function and CryptoExtension
 to contrib.postgres.

---
 django/contrib/postgres/functions.py          | 11 +++++++++-
 django/contrib/postgres/operations.py         |  6 ++++++
 docs/ref/contrib/postgres/functions.txt       | 20 +++++++++++++++++++
 docs/ref/contrib/postgres/operations.txt      |  9 +++++++++
 docs/releases/2.0.txt                         |  6 ++++++
 .../migrations/0001_setup_extensions.py       |  6 ++++--
 .../migrations/0002_create_test_models.py     |  7 +++++++
 tests/postgres_tests/models.py                |  4 ++++
 tests/postgres_tests/test_functions.py        | 17 ++++++++++++++--
 9 files changed, 81 insertions(+), 5 deletions(-)

diff --git a/django/contrib/postgres/functions.py b/django/contrib/postgres/functions.py
index d17f9cb37d..36b32e0751 100644
--- a/django/contrib/postgres/functions.py
+++ b/django/contrib/postgres/functions.py
@@ -1,4 +1,13 @@
-from django.db.models import DateTimeField, Func
+from django.db.models import DateTimeField, Func, UUIDField
+
+
+class RandomUUID(Func):
+    template = 'GEN_RANDOM_UUID()'
+
+    def __init__(self, output_field=None, **extra):
+        if output_field is None:
+            output_field = UUIDField()
+        super().__init__(output_field=output_field, **extra)
 
 
 class TransactionNow(Func):
diff --git a/django/contrib/postgres/operations.py b/django/contrib/postgres/operations.py
index 7544e38613..8ff043b5a9 100644
--- a/django/contrib/postgres/operations.py
+++ b/django/contrib/postgres/operations.py
@@ -35,6 +35,12 @@ class CITextExtension(CreateExtension):
         self.name = 'citext'
 
 
+class CryptoExtension(CreateExtension):
+
+    def __init__(self):
+        self.name = 'pgcrypto'
+
+
 class HStoreExtension(CreateExtension):
 
     def __init__(self):
diff --git a/docs/ref/contrib/postgres/functions.txt b/docs/ref/contrib/postgres/functions.txt
index 465d423f6d..8d3df51864 100644
--- a/docs/ref/contrib/postgres/functions.txt
+++ b/docs/ref/contrib/postgres/functions.txt
@@ -7,6 +7,26 @@ All of these functions are available from the
 
 .. currentmodule:: django.contrib.postgres.functions
 
+``RandomUUID``
+==============
+
+.. class:: RandomUUID()
+
+.. versionadded:: 2.0
+
+Returns a version 4 UUID.
+
+The `pgcrypto extension`_ must be installed. You can use the
+:class:`~django.contrib.postgres.operations.CryptoExtension` migration
+operation to install it.
+
+.. _pgcrypto extension: https://www.postgresql.org/docs/current/static/pgcrypto.html
+
+Usage example::
+
+    >>> from django.contrib.postgres.functions import RandomUUID
+    >>> Article.objects.update(uuid=RandomUUID())
+
 ``TransactionNow``
 ==================
 
diff --git a/docs/ref/contrib/postgres/operations.txt b/docs/ref/contrib/postgres/operations.txt
index 90cd007121..d984d9a3f7 100644
--- a/docs/ref/contrib/postgres/operations.txt
+++ b/docs/ref/contrib/postgres/operations.txt
@@ -67,6 +67,15 @@ run the query ``CREATE EXTENSION IF NOT EXISTS hstore;``.
 
     Installs the ``citext`` extension.
 
+``CryptoExtension``
+===================
+
+.. class:: CryptoExtension()
+
+    .. versionadded:: 2.0
+
+    Installs the ``pgcrypto`` extension.
+
 ``HStoreExtension``
 ===================
 
diff --git a/docs/releases/2.0.txt b/docs/releases/2.0.txt
index 4babd84773..cc1248a9b0 100644
--- a/docs/releases/2.0.txt
+++ b/docs/releases/2.0.txt
@@ -87,6 +87,12 @@ Minor features
   :class:`~django.contrib.postgres.aggregates.ArrayAgg` determines if
   concatenated values will be distinct.
 
+* The new :class:`~django.contrib.postgres.functions.RandomUUID` database
+  function returns a version 4 UUID. It requires use of PostgreSQL's
+  ``pgcrypto`` extension which can be activated using the new
+  :class:`~django.contrib.postgres.operations.CryptoExtension` migration
+  operation.
+
 :mod:`django.contrib.redirects`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
diff --git a/tests/postgres_tests/migrations/0001_setup_extensions.py b/tests/postgres_tests/migrations/0001_setup_extensions.py
index d090ff7fd6..b00c6c0838 100644
--- a/tests/postgres_tests/migrations/0001_setup_extensions.py
+++ b/tests/postgres_tests/migrations/0001_setup_extensions.py
@@ -4,13 +4,14 @@ from django.db import migrations
 
 try:
     from django.contrib.postgres.operations import (
-        BtreeGinExtension, CITextExtension, CreateExtension, HStoreExtension,
-        TrigramExtension, UnaccentExtension,
+        BtreeGinExtension, CITextExtension, CreateExtension, CryptoExtension,
+        HStoreExtension, TrigramExtension, UnaccentExtension,
     )
 except ImportError:
     BtreeGinExtension = mock.Mock()
     CITextExtension = mock.Mock()
     CreateExtension = mock.Mock()
+    CryptoExtension = mock.Mock()
     HStoreExtension = mock.Mock()
     TrigramExtension = mock.Mock()
     UnaccentExtension = mock.Mock()
@@ -24,6 +25,7 @@ class Migration(migrations.Migration):
         # Ensure CreateExtension quotes extension names by creating one with a
         # dash in its name.
         CreateExtension('uuid-ossp'),
+        CryptoExtension(),
         HStoreExtension(),
         TrigramExtension(),
         UnaccentExtension(),
diff --git a/tests/postgres_tests/migrations/0002_create_test_models.py b/tests/postgres_tests/migrations/0002_create_test_models.py
index 842848dd47..4cd37e5e43 100644
--- a/tests/postgres_tests/migrations/0002_create_test_models.py
+++ b/tests/postgres_tests/migrations/0002_create_test_models.py
@@ -191,6 +191,13 @@ class Migration(migrations.Migration):
                 ('when', models.DateTimeField(null=True, default=None)),
             ]
         ),
+        migrations.CreateModel(
+            name='UUIDTestModel',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('uuid', models.UUIDField(default=None, null=True)),
+            ]
+        ),
         migrations.CreateModel(
             name='RangesModel',
             fields=[
diff --git a/tests/postgres_tests/models.py b/tests/postgres_tests/models.py
index 15fb5ab4ee..001ed00d0c 100644
--- a/tests/postgres_tests/models.py
+++ b/tests/postgres_tests/models.py
@@ -171,3 +171,7 @@ class StatTestModel(models.Model):
 
 class NowTestModel(models.Model):
     when = models.DateTimeField(null=True, default=None)
+
+
+class UUIDTestModel(models.Model):
+    uuid = models.UUIDField(default=None, null=True)
diff --git a/tests/postgres_tests/test_functions.py b/tests/postgres_tests/test_functions.py
index 620b561325..875a4b9520 100644
--- a/tests/postgres_tests/test_functions.py
+++ b/tests/postgres_tests/test_functions.py
@@ -1,10 +1,11 @@
+import uuid
 from datetime import datetime
 from time import sleep
 
-from django.contrib.postgres.functions import TransactionNow
+from django.contrib.postgres.functions import RandomUUID, TransactionNow
 
 from . import PostgreSQLTestCase
-from .models import NowTestModel
+from .models import NowTestModel, UUIDTestModel
 
 
 class TestTransactionNow(PostgreSQLTestCase):
@@ -26,3 +27,15 @@ class TestTransactionNow(PostgreSQLTestCase):
 
         self.assertIsInstance(m1.when, datetime)
         self.assertEqual(m1.when, m2.when)
+
+
+class TestRandomUUID(PostgreSQLTestCase):
+
+    def test_random_uuid(self):
+        m1 = UUIDTestModel.objects.create()
+        m2 = UUIDTestModel.objects.create()
+        UUIDTestModel.objects.update(uuid=RandomUUID())
+        m1.refresh_from_db()
+        m2.refresh_from_db()
+        self.assertIsInstance(m1.uuid, uuid.UUID)
+        self.assertNotEqual(m1.uuid, m2.uuid)