From af1fa5e7da21c57a4037e67f93493af4e78d454a Mon Sep 17 00:00:00 2001
From: Pavel Kulikov <kulikovpavel@gmail.com>
Date: Sun, 26 Mar 2017 22:29:05 +0300
Subject: [PATCH] Fixed #27978 -- Allowed loaddata to read data from stdin.

Thanks Squareweave for the django-loaddata-stdin project from which this
is adapted.
---
 AUTHORS                                     |  1 +
 django/core/management/commands/loaddata.py | 17 ++++++++++++
 docs/ref/django-admin.txt                   | 29 +++++++++++++++++++++
 docs/releases/2.0.txt                       |  2 ++
 tests/fixtures/tests.py                     | 29 +++++++++++++++++++++
 5 files changed, 78 insertions(+)

diff --git a/AUTHORS b/AUTHORS
index 30a445b91c..fbf66bacbf 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -617,6 +617,7 @@ answer newbie questions, and generally made Django that much better:
     Paulo Poiati <paulogpoiati@gmail.com>
     Paulo Scardine <paulo@scardine.com.br>
     Paul Smith <blinkylights23@gmail.com>
+    Pavel Kulikov <kulikovpavel@gmail.com>
     pavithran s <pavithran.s@gmail.com>
     Pavlo Kapyshin <i@93z.org>
     permonik@mesias.brnonet.cz
diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py
index 395d459378..2c19350cc4 100644
--- a/django/core/management/commands/loaddata.py
+++ b/django/core/management/commands/loaddata.py
@@ -2,6 +2,7 @@ import functools
 import glob
 import gzip
 import os
+import sys
 import warnings
 import zipfile
 from itertools import product
@@ -25,6 +26,8 @@ try:
 except ImportError:
     has_bz2 = False
 
+READ_STDIN = '-'
+
 
 class Command(BaseCommand):
     help = 'Installs the named fixture(s) in the database.'
@@ -52,6 +55,10 @@ class Command(BaseCommand):
             '-e', '--exclude', dest='exclude', action='append', default=[],
             help='An app_label or app_label.ModelName to exclude. Can be used multiple times.',
         )
+        parser.add_argument(
+            '--format', action='store', dest='format', default=None,
+            help='Format of serialized data when reading from stdin.',
+        )
 
     def handle(self, *fixture_labels, **options):
         self.ignore = options['ignore']
@@ -59,6 +66,7 @@ class Command(BaseCommand):
         self.app_label = options['app_label']
         self.verbosity = options['verbosity']
         self.excluded_models, self.excluded_apps = parse_apps_and_model_labels(options['exclude'])
+        self.format = options['format']
 
         with transaction.atomic(using=self.using):
             self.loaddata(fixture_labels)
@@ -85,6 +93,7 @@ class Command(BaseCommand):
             None: (open, 'rb'),
             'gz': (gzip.GzipFile, 'rb'),
             'zip': (SingleZipReader, 'r'),
+            'stdin': (lambda *args: sys.stdin, None),
         }
         if has_bz2:
             self.compression_formats['bz2'] = (bz2.BZ2File, 'r')
@@ -201,6 +210,9 @@ class Command(BaseCommand):
     @functools.lru_cache(maxsize=None)
     def find_fixtures(self, fixture_label):
         """Find fixture files for a given label."""
+        if fixture_label == READ_STDIN:
+            return [(READ_STDIN, None, READ_STDIN)]
+
         fixture_name, ser_fmt, cmp_fmt = self.parse_name(fixture_label)
         databases = [self.using, None]
         cmp_fmts = list(self.compression_formats.keys()) if cmp_fmt is None else [cmp_fmt]
@@ -288,6 +300,11 @@ class Command(BaseCommand):
         """
         Split fixture name in name, serialization format, compression format.
         """
+        if fixture_name == READ_STDIN:
+            if not self.format:
+                raise CommandError('--format must be specified when reading from stdin.')
+            return READ_STDIN, self.format, 'stdin'
+
         parts = fixture_name.rsplit('.', 2)
 
         if len(parts) > 1 and parts[-1] in self.compression_formats:
diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt
index 2de0ab6a8b..68fb762623 100644
--- a/docs/ref/django-admin.txt
+++ b/docs/ref/django-admin.txt
@@ -416,6 +416,14 @@ originally generated.
 
 Specifies a single app to look for fixtures in rather than looking in all apps.
 
+.. django-admin-option:: --format FORMAT
+
+.. versionadded:: 2.0
+
+Specifies the :ref:`serialization format <serialization-formats>` (e.g.,
+``json`` or ``xml``) for fixtures :ref:`read from stdin
+<loading-fixtures-stdin>`.
+
 .. django-admin-option:: --exclude EXCLUDE, -e EXCLUDE
 
 .. versionadded:: 1.11
@@ -552,6 +560,27 @@ defined, name the fixture ``mydata.master.json`` or
 ``mydata.master.json.gz`` and the fixture will only be loaded when you
 specify you want to load data into the ``master`` database.
 
+.. _loading-fixtures-stdin:
+
+Loading fixtures from ``stdin``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 2.0
+
+You can use a dash as the fixture name to load input from ``sys.stdin``. For
+example::
+
+    django-admin loaddata --format=json -
+
+When reading from ``stdin``, the :option:`--format <loaddata --format>` option
+is required to specify the :ref:`serialization format <serialization-formats>`
+of the input (e.g., ``json`` or ``xml``).
+
+Loading from ``stdin`` is useful with standard input and output redirections.
+For example::
+
+    django-admin dumpdata --format=json --database=test app_label.ModelName | django-admin loaddata --format=json --database=prod -
+
 ``makemessages``
 ----------------
 
diff --git a/docs/releases/2.0.txt b/docs/releases/2.0.txt
index 186f64319d..de5b38a098 100644
--- a/docs/releases/2.0.txt
+++ b/docs/releases/2.0.txt
@@ -185,6 +185,8 @@ Management Commands
 * The new :option:`makemessages --add-location` option controls the comment
   format in PO files.
 
+* :djadmin:`loaddata` can now :ref:`read from stdin <loading-fixtures-stdin>`.
+
 Migrations
 ~~~~~~~~~~
 
diff --git a/tests/fixtures/tests.py b/tests/fixtures/tests.py
index fe047e9838..fb9ba91d3d 100644
--- a/tests/fixtures/tests.py
+++ b/tests/fixtures/tests.py
@@ -680,6 +680,35 @@ class FixtureLoadingTests(DumpDataAssertMixin, TestCase):
         with self.assertRaisesMessage(management.CommandError, msg):
             management.call_command('loaddata', 'fixture1', exclude=['fixtures.FooModel'], verbosity=0)
 
+    def test_stdin_without_format(self):
+        """Reading from stdin raises an error if format isn't specified."""
+        msg = '--format must be specified when reading from stdin.'
+        with self.assertRaisesMessage(management.CommandError, msg):
+            management.call_command('loaddata', '-', verbosity=0)
+
+    def test_loading_stdin(self):
+        """Loading fixtures from stdin with json and xml."""
+        tests_dir = os.path.dirname(__file__)
+        fixture_json = os.path.join(tests_dir, 'fixtures', 'fixture1.json')
+        fixture_xml = os.path.join(tests_dir, 'fixtures', 'fixture3.xml')
+
+        with mock.patch('django.core.management.commands.loaddata.sys.stdin', open(fixture_json, 'r')):
+            management.call_command('loaddata', '--format=json', '-', verbosity=0)
+            self.assertEqual(Article.objects.count(), 2)
+            self.assertQuerysetEqual(Article.objects.all(), [
+                '<Article: Time to reform copyright>',
+                '<Article: Poker has no place on ESPN>',
+            ])
+
+        with mock.patch('django.core.management.commands.loaddata.sys.stdin', open(fixture_xml, 'r')):
+            management.call_command('loaddata', '--format=xml', '-', verbosity=0)
+            self.assertEqual(Article.objects.count(), 3)
+            self.assertQuerysetEqual(Article.objects.all(), [
+                '<Article: XML identified as leading cause of cancer>',
+                '<Article: Time to reform copyright>',
+                '<Article: Poker on TV is great!>',
+            ])
+
 
 class NonexistentFixtureTests(TestCase):
     """