From 301d1bc7f52fffc18b8cb34fbde01633bc1599ee Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Martin=20Sandstr=C3=B6m?= <martin@marteinn.se>
Date: Mon, 11 May 2020 19:19:20 +0200
Subject: [PATCH] Add ability to import redirects from a file
 wagtail.contrib.redirects

* Add support for importing redirects via tsv, csv, xls and xlsx files
* Add import_redirects management command to redirects documentation
---
 CHANGELOG.txt                                 |   1 +
 docs/reference/contrib/redirects.rst          |  42 +-
 docs/releases/2.10.rst                        |   1 +
 setup.py                                      |   1 +
 wagtail/contrib/redirects/base_formats.py     | 228 ++++++++++
 wagtail/contrib/redirects/forms.py            |  59 +++
 .../contrib/redirects/management/__init__.py  |   0
 .../redirects/management/commands/__init__.py |   0
 .../management/commands/import_redirects.py   | 186 ++++++++
 .../static/wagtailredirects/css/index.css     |  17 +
 .../wagtailredirects/choose_import_file.html  |  52 +++
 .../wagtailredirects/confirm_import.html      |  58 +++
 .../wagtailredirects/import_summary.html      |  34 ++
 .../templates/wagtailredirects/index.html     |  38 +-
 .../contrib/redirects/tests/files/example.csv |   4 +
 .../redirects/tests/files/example.json        |  14 +
 .../redirects/tests/files/example.numbers     | Bin 0 -> 88518 bytes
 .../contrib/redirects/tests/files/example.tsv |   4 +
 .../contrib/redirects/tests/files/example.xls | Bin 0 -> 8704 bytes
 .../redirects/tests/files/example.xlsx        | Bin 0 -> 5949 bytes
 .../redirects/tests/files/example.yaml        |   5 +
 .../redirects/tests/files/example_faulty.csv  | Bin 0 -> 8704 bytes
 .../tests/test_import_admin_views.py          | 415 ++++++++++++++++++
 .../redirects/tests/test_import_command.py    | 335 ++++++++++++++
 .../redirects/tests/test_import_forms.py      |  23 +
 .../redirects/tests/test_import_utils.py      |  59 +++
 .../{tests.py => tests/test_redirects.py}     |   0
 wagtail/contrib/redirects/tmp_storages.py     | 117 +++++
 wagtail/contrib/redirects/urls.py             |   2 +
 wagtail/contrib/redirects/utils.py            |  54 +++
 wagtail/contrib/redirects/views.py            | 225 +++++++++-
 31 files changed, 1967 insertions(+), 7 deletions(-)
 create mode 100644 wagtail/contrib/redirects/base_formats.py
 create mode 100644 wagtail/contrib/redirects/management/__init__.py
 create mode 100644 wagtail/contrib/redirects/management/commands/__init__.py
 create mode 100644 wagtail/contrib/redirects/management/commands/import_redirects.py
 create mode 100644 wagtail/contrib/redirects/static/wagtailredirects/css/index.css
 create mode 100644 wagtail/contrib/redirects/templates/wagtailredirects/choose_import_file.html
 create mode 100644 wagtail/contrib/redirects/templates/wagtailredirects/confirm_import.html
 create mode 100644 wagtail/contrib/redirects/templates/wagtailredirects/import_summary.html
 create mode 100644 wagtail/contrib/redirects/tests/files/example.csv
 create mode 100644 wagtail/contrib/redirects/tests/files/example.json
 create mode 100755 wagtail/contrib/redirects/tests/files/example.numbers
 create mode 100644 wagtail/contrib/redirects/tests/files/example.tsv
 create mode 100644 wagtail/contrib/redirects/tests/files/example.xls
 create mode 100644 wagtail/contrib/redirects/tests/files/example.xlsx
 create mode 100644 wagtail/contrib/redirects/tests/files/example.yaml
 create mode 100644 wagtail/contrib/redirects/tests/files/example_faulty.csv
 create mode 100644 wagtail/contrib/redirects/tests/test_import_admin_views.py
 create mode 100644 wagtail/contrib/redirects/tests/test_import_command.py
 create mode 100644 wagtail/contrib/redirects/tests/test_import_forms.py
 create mode 100644 wagtail/contrib/redirects/tests/test_import_utils.py
 rename wagtail/contrib/redirects/{tests.py => tests/test_redirects.py} (100%)
 create mode 100644 wagtail/contrib/redirects/tmp_storages.py
 create mode 100644 wagtail/contrib/redirects/utils.py

diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 01482121ad..e4759a7690 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -38,6 +38,7 @@ Changelog
  * Remove markup around rich text rendering by default, provide a way to use old behaviour via `wagtail.contrib.legacy.richtext` (Coen van der Kamp, Dan Braghis)
  * Apply title length normalisation to improve ranking on PostgreSQL search (Karl Hobley)
  * Add `WAGTAIL_TIME_FORMAT` setting (Jacob Topp-Mugglestone)
+ * Add ability to import redirects from an uploaded file (CSV, TSV, XLX, and XLXS) (Martin Sandström)
  * Fix: Support IPv6 domain (Alex Gleason, Coen van der Kamp)
  * Fix: Ensure link to add a new user works when no users are visible in the users list (LB (Ben Johnston))
  * Fix: `AbstractEmailForm` saved submission fields are now aligned with the email content fields, `form.cleaned_data` will be used instead of `form.fields` (Haydn Greatnews)
diff --git a/docs/reference/contrib/redirects.rst b/docs/reference/contrib/redirects.rst
index e8e3e52af5..5f9c2ff712 100644
--- a/docs/reference/contrib/redirects.rst
+++ b/docs/reference/contrib/redirects.rst
@@ -21,10 +21,10 @@ The ``redirects`` module is not enabled by default. To install it, add ``wagtail
 
         'wagtail.contrib.redirects',
     ]
-    
+
     MIDDLEWARE = [
         # ...
-        # all other django middlware first 
+        # all other django middlware first
 
         'wagtail.contrib.redirects.middleware.RedirectMiddleware',
     ]
@@ -40,6 +40,44 @@ Page model recipe of to have redirects created automatically when changing a pag
 
 For an editor's guide to the interface, see :ref:`managing_redirects`.
 
+
+Management commands
+===================
+
+import_redirects
+----------------
+
+.. code-block:: console
+
+    $ ./manage.py import_redirects
+
+This command imports and creates redirects from a file supplied by the user.
+
+Options:
+
+ - **src**
+   This is the path to the file you wish to import redirects from.
+
+ - **site**
+   This is the **site** for the site you wish to save redirects to.
+
+ - **permanent**
+   If the redirects imported should be **permanent** (True) or not (False). It's True by default.
+
+ - **from**
+   The column index you want to use as redirect from value.
+
+ - **to**
+   The column index you want to use as redirect to value.
+
+ - **dry_run**
+   Lets you run a import without doing any changes.
+
+ - **ask**
+   Lets you inspect and approve each redirect before it is created.
+
+
+
 The ``Redirect`` class
 ======================
 
diff --git a/docs/releases/2.10.rst b/docs/releases/2.10.rst
index 454a5787dc..47bdd6d663 100644
--- a/docs/releases/2.10.rst
+++ b/docs/releases/2.10.rst
@@ -51,6 +51,7 @@ Other features
  * Remove markup around rich text rendering by default, provide a way to use old behaviour via ``wagtail.contrib.legacy.richtext``. See :ref:`legacy_richtext`. (Coen van der Kamp, Dan Braghis)
  * Add ``WAGTAIL_TIME_FORMAT`` setting (Jacob Topp-Mugglestone)
  * Apply title length normalisation to improve ranking on PostgreSQL search (Karl Hobley)
+ * Add ability to import redirects from an uploaded file (CSV, TSV, XLX, and XLXS) (Martin Sandström)
 
 
 Bug fixes
diff --git a/setup.py b/setup.py
index e44a9f467f..5e40c6bdcd 100755
--- a/setup.py
+++ b/setup.py
@@ -36,6 +36,7 @@ install_requires = [
     "requests>=2.11.1,<3.0",
     "l18n>=2018.5",
     "xlsxwriter>=1.2.8,<2.0",
+    "tablib[xls,xlsx]>=0.14.0",
 ]
 
 # Testing dependencies
diff --git a/wagtail/contrib/redirects/base_formats.py b/wagtail/contrib/redirects/base_formats.py
new file mode 100644
index 0000000000..a56f1a8286
--- /dev/null
+++ b/wagtail/contrib/redirects/base_formats.py
@@ -0,0 +1,228 @@
+"""
+Copyright (c) Bojan Mihelac and individual contributors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    1. Redistributions of source code must retain the above copyright notice,
+       this list of conditions and the following disclaimer.
+
+    2. Redistributions in binary form must reproduce the above copyright
+       notice, this list of conditions and the following disclaimer in the
+       documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+# https://raw.githubusercontent.com/django-import-export/django-import-export/master/import_export/formats/base_formats.py
+from importlib import import_module
+import tablib
+
+
+class Format:
+    def get_title(self):
+        return type(self)
+
+    def create_dataset(self, in_stream):
+        """
+        Create dataset from given string.
+        """
+        raise NotImplementedError()
+
+    def export_data(self, dataset, **kwargs):
+        """
+        Returns format representation for given dataset.
+        """
+        raise NotImplementedError()
+
+    def is_binary(self):
+        """
+        Returns if this format is binary.
+        """
+        return True
+
+    def get_read_mode(self):
+        """
+        Returns mode for opening files.
+        """
+        return 'rb'
+
+    def get_extension(self):
+        """
+        Returns extension for this format files.
+        """
+        return ""
+
+    def get_content_type(self):
+        # For content types see
+        # https://www.iana.org/assignments/media-types/media-types.xhtml
+        return 'application/octet-stream'
+
+    @classmethod
+    def is_available(cls):
+        return True
+
+    def can_import(self):
+        return False
+
+    def can_export(self):
+        return False
+
+
+class TablibFormat(Format):
+    TABLIB_MODULE = None
+    CONTENT_TYPE = 'application/octet-stream'
+
+    def get_format(self):
+        """
+        Import and returns tablib module.
+        """
+        try:
+            # Available since tablib 1.0
+            from tablib.formats import registry
+        except ImportError:
+            return import_module(self.TABLIB_MODULE)
+        else:
+            key = self.TABLIB_MODULE.split('.')[-1].replace('_', '')
+            return registry.get_format(key)
+
+    @classmethod
+    def is_available(cls):
+        try:
+            cls().get_format()
+        except (tablib.core.UnsupportedFormat, ImportError):
+            return False
+        return True
+
+    def get_title(self):
+        return self.get_format().title
+
+    def create_dataset(self, in_stream, **kwargs):
+        return tablib.import_set(in_stream, format=self.get_title())
+
+    def export_data(self, dataset, **kwargs):
+        return dataset.export(self.get_title(), **kwargs)
+
+    def get_extension(self):
+        return self.get_format().extensions[0]
+
+    def get_content_type(self):
+        return self.CONTENT_TYPE
+
+    def can_import(self):
+        return hasattr(self.get_format(), 'import_set')
+
+    def can_export(self):
+        return hasattr(self.get_format(), 'export_set')
+
+
+class TextFormat(TablibFormat):
+    def get_read_mode(self):
+        return 'r'
+
+    def is_binary(self):
+        return False
+
+
+class CSV(TextFormat):
+    TABLIB_MODULE = 'tablib.formats._csv'
+    CONTENT_TYPE = 'text/csv'
+
+    def create_dataset(self, in_stream, **kwargs):
+        return super().create_dataset(in_stream, **kwargs)
+
+
+class JSON(TextFormat):
+    TABLIB_MODULE = 'tablib.formats._json'
+    CONTENT_TYPE = 'application/json'
+
+
+class YAML(TextFormat):
+    TABLIB_MODULE = 'tablib.formats._yaml'
+    # See https://stackoverflow.com/questions/332129/yaml-mime-type
+    CONTENT_TYPE = 'text/yaml'
+
+
+class TSV(TextFormat):
+    TABLIB_MODULE = 'tablib.formats._tsv'
+    CONTENT_TYPE = 'text/tab-separated-values'
+
+    def create_dataset(self, in_stream, **kwargs):
+        return super().create_dataset(in_stream, **kwargs)
+
+
+class ODS(TextFormat):
+    TABLIB_MODULE = 'tablib.formats._ods'
+    CONTENT_TYPE = 'application/vnd.oasis.opendocument.spreadsheet'
+
+
+class HTML(TextFormat):
+    TABLIB_MODULE = 'tablib.formats._html'
+    CONTENT_TYPE = 'text/html'
+
+
+class XLS(TablibFormat):
+    TABLIB_MODULE = 'tablib.formats._xls'
+    CONTENT_TYPE = 'application/vnd.ms-excel'
+
+    def create_dataset(self, in_stream):
+        """
+        Create dataset from first sheet.
+        """
+        import xlrd
+        xls_book = xlrd.open_workbook(file_contents=in_stream)
+        dataset = tablib.Dataset()
+        sheet = xls_book.sheets()[0]
+
+        dataset.headers = sheet.row_values(0)
+        for i in range(1, sheet.nrows):
+            dataset.append(sheet.row_values(i))
+        return dataset
+
+
+class XLSX(TablibFormat):
+    TABLIB_MODULE = 'tablib.formats._xlsx'
+    CONTENT_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+
+    def create_dataset(self, in_stream):
+        """
+        Create dataset from first sheet.
+        """
+        from io import BytesIO
+        import openpyxl
+        xlsx_book = openpyxl.load_workbook(BytesIO(in_stream), read_only=True)
+
+        dataset = tablib.Dataset()
+        sheet = xlsx_book.active
+
+        # obtain generator
+        rows = sheet.rows
+        dataset.headers = [cell.value for cell in next(rows)]
+
+        for row in rows:
+            row_values = [cell.value for cell in row]
+            dataset.append(row_values)
+        return dataset
+
+
+#: These are the default formats for import and export. Whether they can be
+#: used or not is depending on their implementation in the tablib library.
+DEFAULT_FORMATS = [fmt for fmt in (
+    CSV,
+    XLS,
+    XLSX,
+    TSV,
+    ODS,
+    JSON,
+    YAML,
+    HTML,
+) if fmt.is_available()]
diff --git a/wagtail/contrib/redirects/forms.py b/wagtail/contrib/redirects/forms.py
index 4af2061d1c..9437d84ca9 100644
--- a/wagtail/contrib/redirects/forms.py
+++ b/wagtail/contrib/redirects/forms.py
@@ -1,3 +1,5 @@
+import os
+
 from django import forms
 from django.utils.translation import gettext_lazy as _
 
@@ -42,3 +44,60 @@ class RedirectForm(forms.ModelForm):
     class Meta:
         model = Redirect
         fields = ('old_path', 'site', 'is_permanent', 'redirect_page', 'redirect_link')
+
+
+class ImportForm(forms.Form):
+    import_file = forms.FileField(
+        label=_("File to import"),
+    )
+
+    def __init__(self, allowed_extensions, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        accept = ",".join(
+            [".{}".format(x) for x in allowed_extensions]
+        )
+        self.fields["import_file"].widget = forms.FileInput(
+            attrs={"accept": accept}
+        )
+
+        uppercased_extensions = [x.upper() for x in allowed_extensions]
+        allowed_extensions_text = ", ".join(uppercased_extensions)
+        help_text = _(
+            "Supported formats: %(supported_formats)s."
+        ) % {
+            'supported_formats': allowed_extensions_text,
+        }
+        self.fields["import_file"].help_text = help_text
+
+
+class ConfirmImportForm(forms.Form):
+    from_index = forms.ChoiceField(label=_("From field"), choices=(),)
+    to_index = forms.ChoiceField(label=_("To field"), choices=(),)
+    site = forms.ModelChoiceField(
+        label=_("From site"),
+        queryset=Site.objects.all(),
+        required=False,
+        empty_label=_("All sites"),
+    )
+    permanent = forms.BooleanField(initial=True, required=False)
+    import_file_name = forms.CharField(widget=forms.HiddenInput())
+    original_file_name = forms.CharField(widget=forms.HiddenInput())
+    input_format = forms.CharField(widget=forms.HiddenInput())
+
+    def __init__(self, headers, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        choices = []
+        for i, f in enumerate(headers):
+            choices.append([str(i), f])
+        if len(headers) > 1:
+            choices.insert(0, ("", "---"))
+
+        self.fields["from_index"].choices = choices
+        self.fields["to_index"].choices = choices
+
+    def clean_import_file_name(self):
+        data = self.cleaned_data["import_file_name"]
+        data = os.path.basename(data)
+        return data
diff --git a/wagtail/contrib/redirects/management/__init__.py b/wagtail/contrib/redirects/management/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/wagtail/contrib/redirects/management/commands/__init__.py b/wagtail/contrib/redirects/management/commands/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/wagtail/contrib/redirects/management/commands/import_redirects.py b/wagtail/contrib/redirects/management/commands/import_redirects.py
new file mode 100644
index 0000000000..51d8f4b775
--- /dev/null
+++ b/wagtail/contrib/redirects/management/commands/import_redirects.py
@@ -0,0 +1,186 @@
+import os
+
+import tablib
+from django.core.management.base import BaseCommand
+
+from wagtail.contrib.redirects.forms import RedirectForm
+from wagtail.contrib.redirects.utils import get_format_cls_by_extension, get_supported_extensions
+from wagtail.core.models import Site
+
+
+class Command(BaseCommand):
+    help = "Imports redirects from .csv, .xls, .xlsx"
+
+    def add_arguments(self, parser):
+        parser.add_argument(
+            "--src", help="Path to file", type=str, required=True,
+        )
+        parser.add_argument(
+            "--site", help="The site where redirects will be associated", type=int,
+        )
+        parser.add_argument(
+            "--permanent",
+            help="Save redirects as permanent redirects",
+            type=bool,
+            default=True,
+        )
+        parser.add_argument(
+            "--from",
+            help="The column where to read from link",
+            default=0,
+            type=int,
+        )
+        parser.add_argument(
+            "--to", help="The column where to read to link", default=1, type=int,
+        )
+        parser.add_argument(
+            "--dry_run",
+            action="store_true",
+            help="Run only in test mode, will not create redirects",
+        )
+        parser.add_argument(
+            "--dry-run",
+            action="store_true",
+            help="Run only in test mode, will not create redirects",
+        )
+        parser.add_argument(
+            "--ask",
+            help="Ask before creating",
+            action="store_true",
+        )
+        parser.add_argument(
+            "--format",
+            help="Source file format (example: .csv, .xls etc)",
+            choices=get_supported_extensions(),
+            type=str,
+        )
+        parser.add_argument(
+            "--offset", help="Import starting with index", type=int, default=None
+        )
+        parser.add_argument(
+            "--limit", help="Limit import to num items", type=int, default=None
+        )
+
+    def handle(self, *args, **options):
+        src = options["src"]
+        from_index = options.pop("from")
+        to_index = options.pop("to")
+        site_id = options.pop("site", None)
+        permament = options.pop("permanent")
+
+        dry_run = options.pop("dry_run", False) or options.pop("dry-run", False)
+        format_ = options.pop("format", None)
+        ask = options.pop("ask")
+        offset = options.pop("offset")
+        limit = options.pop("limit")
+
+        errors = []
+        successes = 0
+        skipped = 0
+        total = 0
+        site = None
+
+        if site_id:
+            site = Site.objects.get(id=site_id)
+
+        if not os.path.exists(src):
+            raise Exception("Missing file '{0}'".format(src))
+
+        if not os.path.getsize(src) > 0:
+            raise Exception("File '{0}' is empty".format(src))
+
+        _, extension = os.path.splitext(src)
+        extension = extension.lstrip(".")
+
+        if not format_:
+            format_ = extension
+
+        if not get_format_cls_by_extension(format_):
+            raise Exception("Invalid format '{0}'".format(extension))
+
+        if extension in ["xls", "xlsx"]:
+            mode = "rb"
+        else:
+            mode = "r"
+
+        with open(src, mode) as fh:
+            imported_data = tablib.Dataset().load(fh.read(), format=format_)
+
+            sample_data = tablib.Dataset(
+                *imported_data[: min(len(imported_data), 4)],
+                headers=imported_data.headers
+            )
+
+            try:
+                self.stdout.write("Sample data:")
+                self.stdout.write(str(sample_data))
+            except Exception:
+                self.stdout.write("Warning: Cannot display sample data")
+
+            self.stdout.write("--------------")
+
+            if site:
+                self.stdout.write("Using site: {0}".format(site.hostname))
+
+            self.stdout.write("Importing redirects:")
+
+            if offset:
+                imported_data = imported_data[offset:]
+            if limit:
+                imported_data = imported_data[:limit]
+
+            for row in imported_data:
+                total += 1
+
+                from_link = row[from_index]
+                to_link = row[to_index]
+
+                data = {
+                    "old_path": from_link,
+                    "redirect_link": to_link,
+                    "is_permanent": permament,
+                }
+
+                if site:
+                    data["site"] = site.pk
+
+                form = RedirectForm(data)
+                if not form.is_valid():
+                    error = form.errors.as_text().replace("\n", "")
+                    self.stdout.write(
+                        "{}. Error: {} -> {} (Reason: {})".format(
+                            total, from_link, to_link, error,
+                        )
+                    )
+                    errors.append(error)
+                    continue
+
+                if ask:
+                    answer = get_input(
+                        "{}. Found {} -> {} Create? Y/n: ".format(
+                            total, from_link, to_link,
+                        )
+                    )
+
+                    if answer != "Y":
+                        skipped += 1
+                        continue
+                else:
+                    self.stdout.write("{}. {} -> {}".format(total, from_link, to_link,))
+
+                if dry_run:
+                    successes += 1
+                    continue
+
+                form.save()
+                successes += 1
+
+        self.stdout.write("\n")
+        self.stdout.write("Found: {}".format(total))
+        self.stdout.write("Created: {}".format(successes))
+        self.stdout.write("Skipped : {}".format(skipped))
+        self.stdout.write("Errors: {}".format(len(errors)))
+
+
+def get_input(msg):  # pragma: no cover
+    return input(msg)
diff --git a/wagtail/contrib/redirects/static/wagtailredirects/css/index.css b/wagtail/contrib/redirects/static/wagtailredirects/css/index.css
new file mode 100644
index 0000000000..e56ca4fdf7
--- /dev/null
+++ b/wagtail/contrib/redirects/static/wagtailredirects/css/index.css
@@ -0,0 +1,17 @@
+.listing-with-x-scroll {
+    display: block;
+    overflow-x: auto;
+    white-space: nowrap;
+}
+
+header .has-multiple-actions {
+    display: -webkit-box;
+    display: -moz-box;
+    display: -webkit-flex;
+    display: -ms-flexbox;
+    display: flex;
+}
+
+header .has-multiple-actions .actionbutton {
+    margin-left: 10px;
+}
diff --git a/wagtail/contrib/redirects/templates/wagtailredirects/choose_import_file.html b/wagtail/contrib/redirects/templates/wagtailredirects/choose_import_file.html
new file mode 100644
index 0000000000..8ee4898914
--- /dev/null
+++ b/wagtail/contrib/redirects/templates/wagtailredirects/choose_import_file.html
@@ -0,0 +1,52 @@
+{% extends "wagtailadmin/base.html" %}
+{% load i18n %}
+{% block titletag %}{% trans "Redirects" %}{% endblock %}
+
+{% block extra_js %}
+    {{ block.super }}
+    <script>
+        window.headerSearch = {
+            url: "{% url 'wagtailredirects:index' %}",
+            termInput: "#id_q",
+            targetOutput: "#redirects-results"
+        }
+    </script>
+{% endblock %}
+
+{% block content %}
+    {% trans "Import redirects" as header_title %}
+    {% include "wagtailadmin/shared/header.html" with title=header_title icon="redirect" %}
+
+    <div class="nice-padding">
+        <div class="help-block help-info">
+            {% blocktrans %}
+                <p>Select a file where redirects are separated into rows and contains the columns representing <code>from</code> and <code>to</code> (they can be named anything).</p>
+                <p>After submitting you will be taken to a confirmation view where you can customize your redirects before import.</p>
+            {% endblocktrans %}
+        </div>
+    </div>
+
+    {% if form.non_field_errors %}
+        <div class="messages">
+            <ul>
+                {% for error in form.non_field_errors %}
+                    <li class="error">{{ error }}</li>
+                {% endfor %}
+            </ul>
+        </div>
+    {% endif %}
+
+    <form action="" method="POST" class="nice-padding" novalidate enctype="multipart/form-data">
+        {% csrf_token %}
+
+        <ul class="fields">
+            {% for field in form.visible_fields %}
+                {% include "wagtailadmin/shared/field_as_li.html" %}
+            {% endfor %}
+
+            <li>
+                <input type="submit" value="{% trans 'Import' %}" class="button" />
+            </li>
+        </ul>
+    </form>
+{% endblock %}
diff --git a/wagtail/contrib/redirects/templates/wagtailredirects/confirm_import.html b/wagtail/contrib/redirects/templates/wagtailredirects/confirm_import.html
new file mode 100644
index 0000000000..3b53330362
--- /dev/null
+++ b/wagtail/contrib/redirects/templates/wagtailredirects/confirm_import.html
@@ -0,0 +1,58 @@
+{% extends "wagtailadmin/base.html" %}
+{% load i18n %}
+
+{% block titletag %}{% trans "Confirm import" %}{% endblock %}
+{% block content %}
+    {% trans "Import redirects" as header_title %}
+    {% trans "Confirm import" as header_subtitle %}
+    {% include "wagtailadmin/shared/header.html" with title=header_title subtitle=header_subtitle icon="redirect" %}
+
+    {% if form.non_field_errors %}
+        <div class="messages">
+            <ul>
+                {% for error in form.non_field_errors %}
+                    <li class="error">{{ error }}</li>
+                {% endfor %}
+            </ul>
+        </div>
+    {% endif %}
+
+    <form action="{% url 'wagtailredirects:process_import' %}" method="POST" class="nice-padding" novalidate enctype="multipart/form-data">
+        {% csrf_token %}
+
+        {% for field in form.hidden_fields %}{{ field }}{% endfor %}
+
+        <ul class="fields">
+            {% for field in form.visible_fields %}
+                {% include "wagtailadmin/shared/field_as_li.html" %}
+            {% endfor %}
+
+            <li>
+                <input type="submit" value="{% trans 'Confirm' %}" class="button" />
+                <a href="{% url 'wagtailredirects:index' %}" class="button button-secondary">
+                    {% trans 'Cancel' %}
+                </a>
+            </li>
+        </ul>
+
+        <h2>{% trans "Preview" %}</h2>
+        <table class="listing listing-with-x-scroll">
+            <thead>
+                <tr>
+                    {% for column in dataset.headers %}
+                        <td>{{ column }}</td>
+                    {% endfor %}
+                </tr>
+            </thead>
+            <tbody>
+                {% for row in dataset %}
+                    <tr>
+                        {% for column in row %}
+                            <td>{{ column }}</td>
+                        {% endfor %}
+                    </tr>
+                {% endfor %}
+            </tbody>
+        </table>
+    </form>
+{% endblock %}
diff --git a/wagtail/contrib/redirects/templates/wagtailredirects/import_summary.html b/wagtail/contrib/redirects/templates/wagtailredirects/import_summary.html
new file mode 100644
index 0000000000..b958e05b07
--- /dev/null
+++ b/wagtail/contrib/redirects/templates/wagtailredirects/import_summary.html
@@ -0,0 +1,34 @@
+{% extends "wagtailadmin/base.html" %}
+{% load i18n %}
+{% block titletag %}{% trans "Summary" %}{% endblock %}
+{% block content %}
+    {% trans "Import redirects" as header_title %}
+    {% trans "Summary" as header_subtitle %}
+    {% include "wagtailadmin/shared/header.html" with title=header_title subtitle=header_subtitle icon="redirect" %}
+    <section id="summary" class="nice-padding">
+        <p class="help-block help-warning">
+            {% blocktrans with total=import_summary.total successes=import_summary.successes errors=import_summary.errors_count %}Found {{ total }} redirects, created {{ successes }} and found {{ errors }} errors.{% endblocktrans %}
+        </p>
+
+        <table class="listing">
+            <thead>
+                <tr>
+                    <th>{% trans "From" %}</th>
+                    <th>{% trans "To" %}</th>
+                    <th>{% trans "Error" %}</th>
+                </tr>
+            </thead>
+            <tbody>
+                {% for error in import_summary.errors %}
+                    <tr>
+                        {% for value in error %}
+                            <td>{{ value }}</td>
+                        {% endfor %}
+                    </tr>
+                {% endfor %}
+            </tbody>
+        </table>
+
+        <a href="{% url 'wagtailredirects:index' %}" class="button">{% trans "Continue" %}</a>
+    </section>
+{% endblock %}
diff --git a/wagtail/contrib/redirects/templates/wagtailredirects/index.html b/wagtail/contrib/redirects/templates/wagtailredirects/index.html
index 02622597f6..73b7591059 100644
--- a/wagtail/contrib/redirects/templates/wagtailredirects/index.html
+++ b/wagtail/contrib/redirects/templates/wagtailredirects/index.html
@@ -1,5 +1,7 @@
 {% extends "wagtailadmin/base.html" %}
 {% load i18n %}
+{% load static %}
+
 {% block titletag %}{% trans "Redirects" %}{% endblock %}
 
 {% block extra_js %}
@@ -14,11 +16,41 @@
 {% endblock %}
 
 {% block content %}
+    <link rel="stylesheet" type="text/css" href="{% static 'wagtailredirects/css/index.css' %}">
+
     {% trans "Redirects" as redirects_str %}
-    {% trans "Add redirect" as add_str %}
     {% if user_can_add %}
         {% url "wagtailredirects:add" as add_link %}
-        {% include "wagtailadmin/shared/header.html" with title=redirects_str icon="redirect" action_url=add_link action_text=add_str search_url="wagtailredirects:index" %}
+        {% trans "Add redirect" as add_str %}
+        {% url "wagtailredirects:start_import" as import_link %}
+        {% trans "Import redirects" as import_str %}
+
+        <header class="hasform">
+            {% block breadcrumb %}{% endblock %}
+            <div class="row nice-padding">
+                <div class="left">
+                    <div class="col header-title">
+                        <h1 class="icon icon-redirect">{{ redirects_str }}</h1>
+                    </div>
+                    <form class="col search-form" action="{% url "wagtailredirects:index" %}{% if query_parameters %}?{{ query_parameters }}{% endif %}" method="get" novalidate role="search">
+                        <ul class="fields">
+                            {% for field in search_form %}
+                                {% include "wagtailadmin/shared/field_as_li.html" with field=field field_classes="field-small iconfield" input_classes="icon-search" %}
+                            {% endfor %}
+                            <li class="submit visuallyhidden"><input type="submit" value="Search" class="button" /></li>
+                        </ul>
+                    </form>
+                </div>
+                <div class="right has-multiple-actions">
+                    <div class="actionbutton">
+                        <a href="{{ add_link }}" class="button bicolor icon icon-plus">{{ add_str }}</a>
+                    </div>
+                    <div class="actionbutton">
+                        <a href="{{ import_link }}" class="button bicolor icon icon-doc-full-inverse">{{ import_str }}</a>
+                    </div>
+                </div>
+            </div>
+        </header>
     {% else %}
         {% include "wagtailadmin/shared/header.html" with title=redirects_str icon="redirect" search_url="wagtailredirects:index" %}
     {% endif %}
@@ -28,5 +60,5 @@
             {% include "wagtailredirects/results.html" %}
         </div>
     </div>
-    
+
 {% endblock %}
diff --git a/wagtail/contrib/redirects/tests/files/example.csv b/wagtail/contrib/redirects/tests/files/example.csv
new file mode 100644
index 0000000000..e3bfb72f28
--- /dev/null
+++ b/wagtail/contrib/redirects/tests/files/example.csv
@@ -0,0 +1,4 @@
+from,to
+/hello,http://hello.com/random/
+/goodbye,http://hello.com/goodbye/
+/goodbye,/cake/
diff --git a/wagtail/contrib/redirects/tests/files/example.json b/wagtail/contrib/redirects/tests/files/example.json
new file mode 100644
index 0000000000..f47092e3f7
--- /dev/null
+++ b/wagtail/contrib/redirects/tests/files/example.json
@@ -0,0 +1,14 @@
+[
+    {
+        "from": "/hello-from-json",
+        "to": "http://hello.com/random/"
+    },
+    {
+        "from": "/goodbye-from-json",
+        "to": "http://hello.com/goodbye/"
+    },
+    {
+        "from": "/goodbye-from-json",
+        "to": "/goodbye-not-working/"
+    }
+]
diff --git a/wagtail/contrib/redirects/tests/files/example.numbers b/wagtail/contrib/redirects/tests/files/example.numbers
new file mode 100755
index 0000000000000000000000000000000000000000..00c8588c41b5442a4733ecf0484fca367713ca18
GIT binary patch
literal 88518
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

literal 0
HcmV?d00001

diff --git a/wagtail/contrib/redirects/tests/files/example.tsv b/wagtail/contrib/redirects/tests/files/example.tsv
new file mode 100644
index 0000000000..79260eee9b
--- /dev/null
+++ b/wagtail/contrib/redirects/tests/files/example.tsv
@@ -0,0 +1,4 @@
+from	to
+/hello	http://hello.com/random/
+/goodbye	http://hello.com/goodbye/
+/goodbye	/cake/
diff --git a/wagtail/contrib/redirects/tests/files/example.xls b/wagtail/contrib/redirects/tests/files/example.xls
new file mode 100644
index 0000000000000000000000000000000000000000..4e587f34695127b1c335f83be8519b1306f07b43
GIT binary patch
literal 8704
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

literal 0
HcmV?d00001

diff --git a/wagtail/contrib/redirects/tests/files/example.xlsx b/wagtail/contrib/redirects/tests/files/example.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..1dc3a156a03c6c68881b03b54b1c57695fb8987e
GIT binary patch
literal 5949
zcmaKQ1z1$?_Vv)xEg@Zkv~)^HOM|3H4I$0YNH@rUGzbh`O2^RMAf*UW5(EAKk!}Y5
zbM-#=6Yljp=b1C_yz{Jep1t4qti8|DP(eW@0RRB^0JcS%8U^okKg=V3i6H|3*Z>m1
zOIM&hh@0zQS0n%d;*Z-$c|zB#9v+<VT|cZf-~74|Uin0m{v7DzW{f?1X5J`H#`i`i
zCww8*Xv!4!3Q9LO`_y&w<7QfwgI~4nkBZ9byKRQfXN4}#j~d%QU3%2}b`L2=!Mch+
zC_mhg$7k52p_@kv86stVvpz+L!3c!WBvj{b5;nN|VP7TY&ZB8l8|R-d3mRrE5nspR
zBJ~L~qI2+85T^>p3>Ytm`zFbY)eO{(L88jy1&r^{U7JKUj~TtD^Ti`yPaqo$p3H5j
zu+(a{ayx{`kMu%E#k(HtpULq1H+8OJ%uQox+{z0o{TI?^MC=cc002TnUM!s~G+dpW
zLEIKju0Sp?2mAk(9S5+gU8oen)7@*V^p*xkI#h$>6z%)SN~V>v^OI&-_(ftA0sbsC
zX)0j3HlNSl3qK6~wJq0PRL(VPGK(A~x_~Mrmmu#S3gGoo$VNy`*&PMtk*_}9rSc^A
zsgDfB3v{7>-!v9FvR)kbTm|eBp0i(M9jyIHJeYYh@jh-x_aiEKcRe*P)*d}$&#q7@
zb_3V$i7%X2)iRR@9U*NZgRvi_3zN2vszB$O*fS#|^ZxCR!E$0uXtMh#w}U?gCvm?)
z1V4lL5d0aunX~iXV$T};h}F$QfV%7pVSp_a`4F(dn@GlK%1#sP=K{at4jFFEKOlS!
zi0k2@{e;&N5Q%H<8k!}pQ%@3@@usIR5xS|By*@#<BcK&Hh?g*k@+`|ZT{d55iv3JP
zx2b<YKiYqTYAtWju-J?VeiZlt?Cz&k#g~Jm9Zt*IVP3o6Q9=(tPyP9c-BSIy@4@Y8
zzNnx4+($$bhgeE{L^NLZ+@4OZcIHk_c7F>dHtwTi4;?}1t~XZQDLPftTMLTsT5?0>
zS^=&_Rt_3C`cm6RJ2BKEJJk=Z!q+Vt=3c6*w@+Z~a75UD49s!J{M@j%cjDv4YWlLW
zZ1Q|00z{V3@4za7LAs`)MM)VZA6oxlqNwGIH=}t?dwzw3dx(0)Ilm3#B2PmhjW6LR
zJ?TyQ&e#*$=j7s86eQ;Sa9m($gT}Xa621&_z4ikKbr=)P8B?CI7H8BM98KBv$&Rpi
z54ee2_Nef7ZHgvy@0z3UIJiWNR9n9)Sk?XlLlSDJW*#Pu$6Y%XIJ%;wmK@3LPIS$5
zNKxIIuhQakd}sZUMvZ8`Au?8q5CzKL8T+jZf9CD~qY)nw8u2T6)u)CYhRSSjL6~92
z^G8UbtqsrYm299-ye}QK70vJEVnBM|)bT;$pKtHFXh70R<GQ4DlX7?(XOD9E_S$`$
ze1qNF^xIZV4Z!ic#BAx9mYsaX*5yOqRGg%|X7O2q8<RDI3!9dUOCBYu!1IzrAo}OT
zXY~#^FbD0RIclQohhhy5yUxUm1Cc?MIyMJ_7Ki-?T(U&=pa^?YbFu`i3(-;6=j4(&
zUP>;Xp<BD(KUk;tfi5+#)2BJZBSa|L&6g4tV0;5Z7k-=uYCJv%Yg>Kzw>Our6pcA5
zIsmYv3jh%RPL-Pt&;iK(>&pAL4Yp(K;508taJ}@B{-5oM;RNOmRQKq*s7WMSYbJ`5
z^b|HDTsd2&ESB?QF|FN{%*OIh?z19Nu1vK1;DHE*eH++L%Vns6r_P}8I^Vn>Ulr$Y
zj~nUX@#yuT)wPd#hnU}SN@fYJY6+SRm-^u75^HjnFc>E{V^GLeUZhU0R53C*PhX5(
zx?P4F6V%#e3S(m0@Yrt^hMn_Auf}H=uL$NEuV>vGjzm^jXBjJSa7ma*hi4P|+d)zU
zO?b*^<C_^QS4H{5cV+aT71xY?KIcK0+u*8n2_D*c5x;F6Kv8pJ(>X~aa#i!OXydZX
zjSk$~JK)8FcTParHBMkCJx<<uKVRk4B4FJd>g4Ihdp*%FbV2K}$9VesF;)Fu59O|>
zpNq!|ZrlB(%6;j$UHSHa$!-h*|Ib}7M?=oPHiil1;KrOg?BYCMDy7pLmOSbHeu&;G
zFM_)Y8wb@#$YY)-M2pnDYeI8$RgtjdXQ1zt)bdc!r6StqThuBK&(t`;p-WOn&ZN##
z8VzVis#h!Cq+WY(33;EFR&96O`A;E#|8P>@dl8+CVDF-gG<y{D-10CZY;#MMf~`t@
zhYBGk6wZptsuZ~yJ6m8Ws_3lmrQiy~Mwx!;dN&5YCS*$2M8Y0DwCsVl;Yd+l?$2cn
zV9$OW8wj)<G`EoR(rB+huy{pPpXC1Je300I>vReZW<LAQX={k9p^0<!g0ux#&{EE~
z?7<qSjqNq~=f~xDZ-l{#7}O8N7N>i?S}gstzJBV&wr6MhItr%a4@|S1fWYkL8SHT{
z{YnmyBs=bV%8Tz<CyVW!dbbra5Nl--Hw{4^tFE66*j=`D&iD(D0Wx>%&4pcV<k&M8
zOA9rh%GMVbamg4hDLHNLcd0>uBga={co)~6oO6b6Z#aW`@Gp=D+%7CwsEArAw_2|W
zeWjgk(uO1F^e@QbCGXScPk-@GH0k<br2D4s^QgbXkesG2SJ#G;g>&<JNPWmNbncE_
z!AyyTwI5x}z)b3+Gf}U(X*JI%(O5lU?XAqxsdcgmKRoV8qm-FJ9Ob4eg7NZ$)zK;F
z*rC_9&zeW;w5s?t-NS%2T^)T6wP;ddo4y}bZ>G?3^}_PH>wwKZ`u9F^e56vbS`iNi
zyjChOB#`DGIdaj5BXuyO1WVY`fBnKj8s~RRiT}{hroOyVW}fqAiAdTZ20m7p9Tt_I
zZzau-Jz^E)l!(v97ar&?&(T1ao5mKb=ymVnAZuXGiowJQQLAcd%aH>}%|vOd_bWeG
zoJY%J@5##a7H$%RvNQI+MmtPoFBKHRUKX<ShL@mxon?Q+O9#IQunI)2>yAIBfF*R0
zL5sXmxCMA$ayN*7zzN0W#0IJK$jy3pfaW4knW5cb34Oekmg=>~99rtj(O=f@M|y#F
zVF$++%v)>Ebk8zfUp<veVNUKu(Oa%6q?k@n#B#F#OoF2=B91)9u9SSE%g|ofyfde$
zaaeL*z6j-{ZCYe;N$?+nz(jAlRuwL)Pjq9hNyq0^?43OF(CjK>%PY9TTYa6cI9Dpu
zw?ufh(3O4gqMPZH<Qjbyv&CdKmN@$4y2iyetRmhf2(XE```u_LH!BX&D65&IB$W5g
zNhjHB7tqOf5#oykQ*EwA>VYgHsieV2E|&VBUIj3n!iDha3>9WP<KSCKFsgup{2(Dr
zkyb%oK%xKls9!*V<@bn1K%waOsK|g#D5156guEl~EotRXY>e;FdoAuwHF!h|#?urS
zBp%Pn0(7q-`&lu8@^k*<$v;m;t;WkY1NMCYDc_w9P4LgUX0uXeP~=8xz8<K~pp=0`
zI<@^o2&lJR0c)c-Y-6TnG;G18KCpX5TW=ZD$`rte36Wb9bP7DWaa8BY+Q+)#CW}u}
z%LaB=ZZz`XgWtNX&#12~<S|e6Vu;SDUoSNBRL4<u^-8*;5?>AyKKo%|PAd`|<p;aR
za)RKVMR5O`fcr(k86dlEH&4{hpS)Ik3BOZwH?f6@j}i6O4+8)o|9@Qy$OZ^>19AWQ
z`fD{wPM&vt$3qaldr3n10#+tK4siuo6AgHXsuHW??bIX3OrjDbhh`mZwh)11*VCU3
zqow$ld)(lEU=|PHbge-yZDeJf+c`^;qKFAi9f_2b7;C5gbhw}D%n6Gb2Jm;CX=M#Z
zS{dpUXBV&HU=@kx1ryC^(PJ06JW9=0)TZf4j@(98-a%`Ba>N9CX2F@fuRO^zZ|z#D
zjI8n@jKN8Z^i8T8m7xRufLUM49^j(XP%*(3&U&Afj`+Ag7aFT9oWWc7REBag=I3Us
z6N7yxH=f-zC2<d&qc<k~wK_v|oC45XB2~O;g_-p|Q+$o+jC^LKv7+{;S+*b>%|)rQ
zhw<MD+i(cZa)+A)31JRGmem{V`pe)*N$hetm+{o}e)A05&suEPGEFGF9&>A+<9wIB
z@c|Z?J69V~V^xDypL*=<PE0;jc75(FOdP{xb}6s=@jU<OXHkXT2l~?B$&|WsGspbg
zVtv@~l2VsS3)HldkXo~5iImnY0{uHDw9-=h=i&m3O6`qT?GtjG8}5)9@@n<fXj?(N
zLVe;((a0dID>WWrroJhbe~|N?!2!yz-y@a1ucq~3PxK&m@g|>Kj7xLdF&sZ2Ul}GX
zs^9^Id8_OZn5q!+le8_%M(3<oJ4vA9$%&bpGWy`jna+fZVd<qccaM^8aGJ&CK@0bu
z<or`F4*oFEE;|VpZXiU7$g`eN^B}3<aSuypA2sTxk-O0*fuls{%1%DXAr~R>OYQ#m
zeZCd$8Cv(&eS|#Y@-9^bw_)MCW29T6SPabejoO6v6p6KsKmIX9_mP8qCG4RPsmw0h
zQUmV{X#=pd&UOUqzK`>9sII(u+VjV2-w#pI=@NUA4G_bw(T}Fh39vK2YDjYAbD38|
z!Y69lCu)WPQTnNI6iJ?Eg?wPjZ+zSCRMAXdapLqfvs8b;|54{W`YHfppAff#-G6oF
zgFhl_U1Lydw%B=nwYLKth?$rN;<UjHmrhhfuMh6@VG&6Z`Zb7_)fCa>ll*>sKsIKs
zKuc{mS6fGG(BB%d*mz#NZXO&tPc=<oF&9pWj4gS*FcMWxUii~jKZsg7_m#|~J3G%h
zzI{ry7do9K(|bmgMp?_su1!bXgze@yYS!^?#%j&#`O}!U-N0VXxPsVH)wik%{a({%
zxO56})yWAM1+PV1qRThZ#^_|b8;Ijdsw059<#Y`}Z)%A$b)hO$oF%n9>w3`d<LGI7
zweK33<9vVka*@hC#A?@R+oye1<X<AW@}x!mr)tF2V<BI_?f%o{W#gl9glrrT0|2=H
zj@!-K9{AUQcl3>%-tmz5?^ae_wa+m*%&1A#>kc=hzS0u2twhI>ek}M{P2ptw`-7mT
z(yA#HDrG@LKR`F&xHaPL`3CdlsNo_K@!&3z@W2BdOJ;6{Gq}siPs*i!YqR{6H<F`l
zx>D?WEWx0{E&O`|o0-oGM92izGZL{42n&mAq*OWChA8uOcoB+%rCR{w!8^~(&@DqM
z)psszt5EEeXtyH#93*Vv$&gQY^-Lr=RGK7W@cn=pmzNU|i<%zq-OJ@D9tW}q`#L=I
z(nEZ6S`Mdk6T+n>iRUt3iFJX>of0oJ?3O6L@(75`>a227^v684p=H5lws_BBETs_H
zlff#-4B5ooTN?nDYY(BFT76jHn+jtZF?DTfkzv$NPiKv8aW@v~mtQTl%nemXr%V%_
zjWQ~E(KZ{QX2xJybSjnFYd8CLHGb0A*4^Mf=ujt5@FYwtZz|*iQ`mAS1x8lC2Jvhx
zv9tg*tfP9$@%$~}5sgOzG(j*atGC{mY@hFdOH=J=x*F}$4Ev-v{3Vgmp6ARC#JuOG
z8g8q)DrB5^hC2fru{cEWt1C#V#<18?X#cWTcWzbN(?VZr*U<V>2Q!&(9{Koly7Ps9
zk$PqJ=}_~J@3aO6d(n8L?Mk^eW%iX3?gRzIyqr0kymca3==c1UwVq5pcaN}sw_Qn3
zf?*$t31KRMGvI+aS7Y8u!lG!l6t5;obJ9_LWibVFP%AU9;PJ~AvX5(6KhY-**>VgM
zVA1qHk?;yYt?bCZ+~dZ(mKM!WZWiBHZWi59_6ev<4!y#Z_(%GSXgn!tS}HDKt57xH
zy}4GDCf4*vP49BeP;wls90E>^Di|>Hw@ogf4-C7Y>Yo0OVtU~UGa>gjL>f{xXJL(Y
zV%E<vEDpbp@wW0lu^@c2Nq#|Y&C$T5;!>j<VVvRn!1~p$Lna3=^hI_oPCnORNq{dI
z1de|DX;@f@K1@O=KqKOf>|bwVMzT(hZa_!3mpb0gK+uc7i9mVsh$F(gkUND(v(7)_
zb<@-(jgzxY<<#25Nb^b{UyZOQ?*8UUg;f?$ELEkHL;>-&?pJ%9A($8aiYJ9#1RSwR
z2bQvmb}@Lf2I*5cAX0ybpWmkz66;)Ex7O&U)<I-mON3fqIz`8u$j(^Oqo7ToS}VXA
zD2|Ih&ZIpR<Q8>Exo^oIlIi$}tWAh~>5_p)!RgX{=@pfBIeWHdgzaiA(;!8OJ-)t*
zht`V&J%eb=mi`pu`2oj><nYzrAnb2*%4F1$DQ4XRJ-u50(5<YQ<t=;1@9p!fh~Aq~
zvPn4|{bJpdso{<vVSq<%x#)|7+){dwY2B&QKp0stIF7S;FeWXO?BM8HH3tATpgQL%
z&$mD?x`hUv(I+!0PnFQNh%7hcZDYQFlbSQ0pIgcC5uId3?ROETnKn^KYG2wb(B9Lw
z=;S(bd{m=r)u!{xuh!jkur$XwdmqXy(Al2V1&4ZauR>0mL>DT8TgWD&vt2ySq{HFv
zUbO-2pso}sso~U8xaZG{l7Nzsx+i%<J=_{9NXU|iCH&t;C1U#4@rP}B7kJyr`x8Zu
z*eU<z>)nOl_QL*zlOkmIKk$D$V|UTFy{A9XYY6lmC+aTvwhi+qm=yIF_%Ei+U7p(>
z#h*M<Xuo*wIu>^cZuh2t5=3GDCyM{-RPVCfF42Fo4C4J_`LBX~m*IA?`;(y)|2M;b
lO5a`RZDsfqIz|NeUpk?of`$+g000Yd1|rs~_+MKQ@PAH!7R>+v

literal 0
HcmV?d00001

diff --git a/wagtail/contrib/redirects/tests/files/example.yaml b/wagtail/contrib/redirects/tests/files/example.yaml
new file mode 100644
index 0000000000..c60134d786
--- /dev/null
+++ b/wagtail/contrib/redirects/tests/files/example.yaml
@@ -0,0 +1,5 @@
+---
+items:
+  - one
+  - two
+  - three
diff --git a/wagtail/contrib/redirects/tests/files/example_faulty.csv b/wagtail/contrib/redirects/tests/files/example_faulty.csv
new file mode 100644
index 0000000000000000000000000000000000000000..4e587f34695127b1c335f83be8519b1306f07b43
GIT binary patch
literal 8704
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

literal 0
HcmV?d00001

diff --git a/wagtail/contrib/redirects/tests/test_import_admin_views.py b/wagtail/contrib/redirects/tests/test_import_admin_views.py
new file mode 100644
index 0000000000..89f8fda9e7
--- /dev/null
+++ b/wagtail/contrib/redirects/tests/test_import_admin_views.py
@@ -0,0 +1,415 @@
+import os
+
+from django.core.files.uploadedfile import SimpleUploadedFile
+from django.test import TestCase, override_settings
+from django.urls import reverse
+
+from wagtail.contrib.redirects.models import Redirect
+from wagtail.core.models import Site
+from wagtail.tests.utils import WagtailTestUtils
+
+
+TEST_ROOT = os.path.abspath(os.path.dirname(__file__))
+
+
+@override_settings(
+    ALLOWED_HOSTS=["testserver", "localhost", "test.example.com", "other.example.com"]
+)
+class TestImportAdminViews(TestCase, WagtailTestUtils):
+    def setUp(self):
+        self.login()
+
+    def get(self, params={}):
+        return self.client.get(reverse("wagtailredirects:start_import"), params)
+
+    def post(self, post_data={}, follow=False):
+        return self.client.post(
+            reverse("wagtailredirects:start_import"), post_data, follow=follow
+        )
+
+    def post_import(self, post_data={}, follow=False):
+        return self.client.post(
+            reverse("wagtailredirects:process_import"), post_data, follow=follow
+        )
+
+    def test_request_start_with_get_returns_initial_form(self):
+        response = self.get()
+        self.assertEqual(
+            response.templates[0].name, "wagtailredirects/choose_import_file.html",
+        )
+
+    def test_empty_import_file_returns_error(self):
+        response = self.post({
+            "import_file": "",
+            "input_format": "0",
+        })
+
+        self.assertTrue("import_file" in response.context["form"].errors)
+
+    def test_non_valid_format_returns_error(self):
+        f = "{}/files/example.yaml".format(TEST_ROOT)
+        (_, filename) = os.path.split(f)
+
+        with open(f, "rb") as infile:
+            upload_file = SimpleUploadedFile(filename, infile.read())
+
+            response = self.post(
+                {
+                    "import_file": upload_file,
+                },
+                follow=True,
+            )
+
+            self.assertContains(
+                response, 'File format of type &quot;yaml&quot; is not supported'
+            )
+
+    def test_valid_csv_triggers_confirm_view(self):
+        f = "{}/files/example.csv".format(TEST_ROOT)
+        (_, filename) = os.path.split(f)
+
+        with open(f, "rb") as infile:
+            upload_file = SimpleUploadedFile(filename, infile.read())
+
+            response = self.post(
+                {
+                    "import_file": upload_file,
+                }
+            )
+
+            self.assertEqual(
+                response.templates[0].name,
+                "wagtailredirects/confirm_import.html",
+            )
+            self.assertEqual(len(response.context["dataset"]), 3)
+
+    def test_import_step(self):
+        f = "{}/files/example.csv".format(TEST_ROOT)
+        (_, filename) = os.path.split(f)
+
+        with open(f, "rb") as infile:
+            upload_file = SimpleUploadedFile(filename, infile.read())
+
+            self.assertEqual(Redirect.objects.all().count(), 0)
+
+            response = self.post(
+                {
+                    "import_file": upload_file,
+                }
+            )
+
+            import_response = self.post_import(
+                {
+                    **response.context["form"].initial,
+                    "from_index": 0,
+                    "to_index": 1,
+                    "permanent": True,
+                }
+            )
+
+            self.assertEqual(
+                import_response.templates[0].name,
+                "wagtailredirects/import_summary.html",
+            )
+
+            self.assertEqual(Redirect.objects.all().count(), 2)
+
+    def test_permanent_setting(self):
+        f = "{}/files/example.csv".format(TEST_ROOT)
+        (_, filename) = os.path.split(f)
+
+        with open(f, "rb") as infile:
+            upload_file = SimpleUploadedFile(filename, infile.read())
+
+            self.assertEqual(Redirect.objects.all().count(), 0)
+
+            response = self.post(
+                {
+                    "import_file": upload_file,
+                }
+            )
+
+            import_response = self.post_import(
+                {
+                    **response.context["form"].initial,
+                    "from_index": 0,
+                    "to_index": 1,
+                    "permanent": False,
+                }
+            )
+
+            self.assertEqual(
+                import_response.templates[0].name,
+                "wagtailredirects/import_summary.html",
+            )
+
+            self.assertFalse(Redirect.objects.first().is_permanent)
+
+    def test_site_setting(self):
+        f = "{}/files/example.csv".format(TEST_ROOT)
+        (_, filename) = os.path.split(f)
+
+        default_site = Site.objects.first()
+        new_site = Site.objects.create(
+            hostname="hello.dev", root_page=default_site.root_page,
+        )
+
+        with open(f, "rb") as infile:
+            upload_file = SimpleUploadedFile(filename, infile.read())
+
+            self.assertEqual(Redirect.objects.all().count(), 0)
+
+            response = self.post(
+                {
+                    "import_file": upload_file,
+                }
+            )
+
+            import_response = self.post_import(
+                {
+                    **response.context["form"].initial,
+                    "from_index": 0,
+                    "to_index": 1,
+                    "permanent": False,
+                    "site": new_site.pk,
+                }
+            )
+
+            self.assertEqual(
+                import_response.templates[0].name,
+                "wagtailredirects/import_summary.html",
+            )
+
+            self.assertEqual(Redirect.objects.count(), 2)
+            self.assertEqual(Redirect.objects.first().site, new_site)
+
+    def test_import_xls(self):
+        f = "{}/files/example.xls".format(TEST_ROOT)
+        (_, filename) = os.path.split(f)
+
+        with open(f, "rb") as infile:
+            upload_file = SimpleUploadedFile(filename, infile.read())
+
+            self.assertEqual(Redirect.objects.all().count(), 0)
+
+            response = self.post(
+                {
+                    "import_file": upload_file,
+                }
+            )
+
+            self.assertEqual(
+                response.templates[0].name,
+                "wagtailredirects/confirm_import.html",
+            )
+
+            import_response = self.post_import(
+                {
+                    **response.context["form"].initial,
+                    "from_index": 0,
+                    "to_index": 1,
+                    "permanent": True,
+                },
+                follow=True,
+            )
+
+            self.assertEqual(
+                import_response.templates[0].name,
+                "wagtailredirects/index.html",
+            )
+
+            self.assertEqual(Redirect.objects.all().count(), 3)
+
+    def test_import_xlsx(self):
+        f = "{}/files/example.xlsx".format(TEST_ROOT)
+        (_, filename) = os.path.split(f)
+
+        with open(f, "rb") as infile:
+            upload_file = SimpleUploadedFile(filename, infile.read())
+
+            self.assertEqual(Redirect.objects.all().count(), 0)
+
+            response = self.post(
+                {
+                    "import_file": upload_file,
+                }
+            )
+
+            self.assertEqual(
+                response.templates[0].name,
+                "wagtailredirects/confirm_import.html",
+            )
+
+            import_response = self.post_import(
+                {
+                    **response.context["form"].initial,
+                    "from_index": 0,
+                    "to_index": 1,
+                    "permanent": True,
+                },
+                follow=True,
+            )
+
+            self.assertEqual(
+                import_response.templates[0].name,
+                "wagtailredirects/index.html",
+            )
+
+            self.assertEqual(Redirect.objects.all().count(), 3)
+
+    def test_unicode_error_when_importing(self):
+        f = "{}/files/example_faulty.csv".format(TEST_ROOT)
+        (_, filename) = os.path.split(f)
+
+        with open(f, "rb") as infile:
+            upload_file = SimpleUploadedFile(filename, infile.read())
+
+            self.assertEqual(Redirect.objects.all().count(), 0)
+
+            response = self.post(
+                {
+                    "import_file": upload_file,
+                },
+                follow=True,
+            )
+            self.assertTrue(
+                b"Imported file has a wrong encoding:" in response.content
+            )
+
+    def test_not_valid_method_for_import_file(self):
+        response = self.client.get(reverse("wagtailredirects:process_import"))
+        self.assertEqual(response.status_code, 405)
+
+    def test_error_in_data_renders_confirm_view_on_import(self):
+        f = "{}/files/example.csv".format(TEST_ROOT)
+        (_, filename) = os.path.split(f)
+
+        with open(f, "rb") as infile:
+            upload_file = SimpleUploadedFile(filename, infile.read())
+
+            response = self.post(
+                {
+                    "import_file": upload_file,
+                }
+            )
+
+            self.post_import(
+                {
+                    **response.context["form"].initial,
+                    "from_index": 0,
+                    "to_index": 1,
+                    "permanent": True,
+                    "site": 99,
+                }
+            )
+            self.assertEqual(
+                response.templates[0].name,
+                "wagtailredirects/confirm_import.html",
+            )
+
+    def test_import_tsv(self):
+        f = "{}/files/example.tsv".format(TEST_ROOT)
+        (_, filename) = os.path.split(f)
+
+        with open(f, "rb") as infile:
+            upload_file = SimpleUploadedFile(filename, infile.read())
+
+            self.assertEqual(Redirect.objects.all().count(), 0)
+
+            response = self.post(
+                {
+                    "import_file": upload_file,
+                }
+            )
+
+            self.assertEqual(
+                response.templates[0].name,
+                "wagtailredirects/confirm_import.html",
+            )
+
+            import_response = self.post_import(
+                {
+                    **response.context["form"].initial,
+                    "from_index": 0,
+                    "to_index": 1,
+                    "permanent": True,
+                }
+            )
+
+            self.assertEqual(
+                import_response.templates[0].name,
+                "wagtailredirects/import_summary.html",
+            )
+
+            self.assertEqual(Redirect.objects.all().count(), 2)
+
+    @override_settings(WAGTAIL_REDIRECTS_FILE_STORAGE='cache')
+    def test_import_xlsx_with_cache_store_engine(self):
+        f = "{}/files/example.xlsx".format(TEST_ROOT)
+        (_, filename) = os.path.split(f)
+
+        with open(f, "rb") as infile:
+            upload_file = SimpleUploadedFile(filename, infile.read())
+
+            self.assertEqual(Redirect.objects.all().count(), 0)
+
+            response = self.post(
+                {
+                    "import_file": upload_file,
+                }
+            )
+
+            self.assertEqual(
+                response.templates[0].name,
+                "wagtailredirects/confirm_import.html",
+            )
+
+            import_response = self.post_import(
+                {
+                    **response.context["form"].initial,
+                    "from_index": 0,
+                    "to_index": 1,
+                    "permanent": True,
+                },
+                follow=True,
+            )
+
+            self.assertEqual(
+                import_response.templates[0].name,
+                "wagtailredirects/index.html",
+            )
+
+            self.assertEqual(Redirect.objects.all().count(), 3)
+
+    @override_settings(WAGTAIL_REDIRECTS_FILE_STORAGE='cache')
+    def test_process_validation_works_when_using_plaintext_files_and_cache(self):
+        f = "{}/files/example.csv".format(TEST_ROOT)
+        (_, filename) = os.path.split(f)
+
+        with open(f, "rb") as infile:
+            upload_file = SimpleUploadedFile(filename, infile.read())
+
+            self.assertEqual(Redirect.objects.all().count(), 0)
+
+            response = self.post(
+                {
+                    "import_file": upload_file,
+                }
+            )
+
+            self.assertEqual(
+                response.templates[0].name,
+                "wagtailredirects/confirm_import.html",
+            )
+
+            import_response = self.post_import(
+                {
+                    **response.context["form"].initial,
+                    "permanent": True,
+                }
+            )
+
+            self.assertEqual(
+                import_response.templates[0].name,
+                "wagtailredirects/confirm_import.html",
+            )
diff --git a/wagtail/contrib/redirects/tests/test_import_command.py b/wagtail/contrib/redirects/tests/test_import_command.py
new file mode 100644
index 0000000000..91f32ceadf
--- /dev/null
+++ b/wagtail/contrib/redirects/tests/test_import_command.py
@@ -0,0 +1,335 @@
+import os
+import tempfile
+from io import StringIO
+from unittest.mock import patch
+
+from django.core.management import call_command
+from django.core.management.base import CommandError
+from django.test import TestCase
+
+from wagtail.contrib.redirects.models import Redirect
+from wagtail.core.models import Site
+
+
+TEST_ROOT = os.path.abspath(os.path.dirname(__file__))
+
+
+class TestImportCommand(TestCase):
+    def test_empty_command_raises_errors(self):
+        with self.assertRaises(CommandError):
+            out = StringIO()
+            call_command("import_redirects", stdout=out)
+
+    def test_missing_file_raises_error(self):
+        with self.assertRaisesMessage(Exception, "Missing file 'random'"):
+            out = StringIO()
+            call_command("import_redirects", src="random", stdout=out)
+
+    def test_invalid_extension_raises_error(self):
+        f = "{}/files/example.numbers".format(TEST_ROOT)
+
+        with self.assertRaisesMessage(Exception, "Invalid format 'numbers'"):
+            out = StringIO()
+            call_command("import_redirects", src=f, stdout=out)
+
+    def test_empty_file_raises_error(self):
+        empty_file = tempfile.NamedTemporaryFile()
+
+        with self.assertRaisesMessage(
+            Exception, "File '{}' is empty".format(empty_file.name)
+        ):
+            out = StringIO()
+            call_command("import_redirects", src=empty_file.name, stdout=out)
+
+    def test_header_are_not_imported(self):
+        invalid_file = tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8")
+        invalid_file.write("from,to")
+        invalid_file.seek(0)
+
+        out = StringIO()
+        call_command(
+            "import_redirects", src=invalid_file.name, stdout=out, format="csv"
+        )
+
+        self.assertEqual(Redirect.objects.count(), 0)
+
+    def test_format_gets_picked_up_from_file_extension(self):
+        f = "{}/files/example.csv".format(TEST_ROOT)
+
+        out = StringIO()
+        call_command("import_redirects", src=f, stdout=out)
+        self.assertEqual(Redirect.objects.count(), 2)
+
+    def test_binary_formats_are_supported(self):
+        f = "{}/files/example.xls".format(TEST_ROOT)
+
+        out = StringIO()
+        call_command("import_redirects", src=f, stdout=out)
+        self.assertEqual(Redirect.objects.count(), 3)
+
+    def test_redirect_gets_imported(self):
+        invalid_file = tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8")
+        invalid_file.write("from,to\n")
+        invalid_file.write("/alpha,http://omega.test/")
+        invalid_file.seek(0)
+
+        out = StringIO()
+        call_command(
+            "import_redirects", src=invalid_file.name, stdout=out, format="csv"
+        )
+
+        self.assertEqual(Redirect.objects.count(), 1)
+        redirect = Redirect.objects.first()
+        self.assertEqual(redirect.old_path, "/alpha")
+        self.assertEqual(redirect.redirect_link, "http://omega.test/")
+        self.assertEqual(redirect.is_permanent, True)
+
+    def test_trailing_slash_gets_stripped(self):
+        invalid_file = tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8")
+        invalid_file.write("from,to\n")
+        invalid_file.write("/alpha/,http://omega.test/")
+        invalid_file.seek(0)
+
+        out = StringIO()
+        call_command(
+            "import_redirects", src=invalid_file.name, stdout=out, format="csv"
+        )
+
+        redirect = Redirect.objects.first()
+        self.assertEqual(redirect.old_path, "/alpha")
+        self.assertEqual(redirect.redirect_link, "http://omega.test/")
+
+    def test_site_id_does_not_exist(self):
+        with self.assertRaisesMessage(Exception, "Site matching query does not exist"):
+            out = StringIO()
+            call_command("import_redirects", src="random", site=5, stdout=out)
+
+    def test_redirect_gets_added_to_site(self):
+        invalid_file = tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8")
+        invalid_file.write("from,to\n")
+        invalid_file.write("/alpha/,http://omega.test/")
+        invalid_file.seek(0)
+
+        current_site = Site.objects.first()
+        site = Site.objects.create(
+            hostname="random.test", root_page=current_site.root_page
+        )
+
+        out = StringIO()
+        call_command(
+            "import_redirects",
+            src=invalid_file.name,
+            site=site.pk,
+            stdout=out,
+            format="csv",
+        )
+
+        redirect = Redirect.objects.first()
+        self.assertEqual(redirect.old_path, "/alpha")
+        self.assertEqual(redirect.redirect_link, "http://omega.test/")
+        self.assertEqual(redirect.site, site)
+
+    def test_temporary_redirect(self):
+        invalid_file = tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8")
+        invalid_file.write("from,to\n")
+        invalid_file.write("/alpha/,http://omega.test/")
+        invalid_file.seek(0)
+
+        out = StringIO()
+        call_command(
+            "import_redirects",
+            src=invalid_file.name,
+            permanent=False,
+            stdout=out,
+            format="csv",
+        )
+
+        redirect = Redirect.objects.first()
+        self.assertEqual(redirect.old_path, "/alpha")
+        self.assertEqual(redirect.redirect_link, "http://omega.test/")
+        self.assertEqual(redirect.is_permanent, False)
+
+    def test_duplicate_from_links_get_skipped(self):
+        invalid_file = tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8")
+        invalid_file.write("from,to\n")
+        invalid_file.write("/alpha/,http://omega.test/\n")
+        invalid_file.write("/alpha/,http://omega2.test/\n")
+        invalid_file.seek(0)
+
+        out = StringIO()
+        call_command(
+            "import_redirects",
+            src=invalid_file.name,
+            permanent=False,
+            format="csv",
+            stdout=out,
+        )
+
+        self.assertEqual(Redirect.objects.count(), 1)
+
+    def test_non_absolute_to_links_get_skipped(self):
+        invalid_file = tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8")
+        invalid_file.write("from,to\n")
+        invalid_file.write("/alpha/,/omega.test/\n")
+        invalid_file.seek(0)
+
+        out = StringIO()
+        call_command(
+            "import_redirects",
+            src=invalid_file.name,
+            permanent=False,
+            stdout=out,
+            format="csv",
+        )
+
+        self.assertEqual(Redirect.objects.count(), 0)
+        self.assertIn("Errors: 1", out.getvalue())
+
+    def test_from_links_are_converted_to_relative(self):
+        invalid_file = tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8")
+        invalid_file.write("from,to\n")
+        invalid_file.write("http://alpha.test/alpha/,http://omega.test/\n")
+        invalid_file.seek(0)
+
+        out = StringIO()
+        call_command(
+            "import_redirects", src=invalid_file.name, format="csv", stdout=out
+        )
+
+        self.assertEqual(Redirect.objects.count(), 1)
+        redirect = Redirect.objects.first()
+        self.assertEqual(redirect.old_path, "/alpha")
+        self.assertEqual(redirect.redirect_link, "http://omega.test/")
+
+    def test_column_index_are_used(self):
+        invalid_file = tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8")
+        invalid_file.write("priority,from,year,to\n")
+        invalid_file.write("5,/alpha,2020,http://omega.test/")
+        invalid_file.seek(0)
+
+        out = StringIO()
+        call_command(
+            "import_redirects",
+            "--src={}".format(invalid_file.name),
+            "--from=1",
+            "--to=3",
+            "--format=csv",
+            stdout=out,
+        )
+
+        self.assertEqual(Redirect.objects.count(), 1)
+        redirect = Redirect.objects.first()
+        self.assertEqual(redirect.old_path, "/alpha")
+        self.assertEqual(redirect.redirect_link, "http://omega.test/")
+        self.assertEqual(redirect.is_permanent, True)
+
+    def test_nothing_gets_saved_on_dry_run(self):
+        invalid_file = tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8")
+        invalid_file.write("from,to\n")
+        invalid_file.write("/alpha,http://omega.test/")
+        invalid_file.seek(0)
+
+        out = StringIO()
+        call_command(
+            "import_redirects",
+            src=invalid_file.name,
+            format="csv",
+            dry_run=True,
+            stdout=out,
+        )
+
+        self.assertEqual(Redirect.objects.count(), 0)
+
+    @patch(
+        "wagtail.contrib.redirects.management.commands.import_redirects.get_input",
+        return_value="Y",
+    )
+    def test_successfull_ask_imports_redirect(self, get_input):
+        invalid_file = tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8")
+        invalid_file.write("from,to\n")
+        invalid_file.write("/alpha,http://omega.test/")
+        invalid_file.seek(0)
+
+        out = StringIO()
+        call_command(
+            "import_redirects",
+            src=invalid_file.name,
+            format="csv",
+            ask=True,
+            stdout=out,
+        )
+
+        self.assertEqual(Redirect.objects.count(), 1)
+
+    @patch(
+        "wagtail.contrib.redirects.management.commands.import_redirects.get_input",
+        return_value="N",
+    )
+    def test_native_ask_imports_redirect(self, get_input):
+        invalid_file = tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8")
+        invalid_file.write("from,to\n")
+        invalid_file.write("/alpha,http://omega.test/")
+        invalid_file.seek(0)
+
+        out = StringIO()
+        call_command(
+            "import_redirects",
+            src=invalid_file.name,
+            format="csv",
+            ask=True,
+            stdout=out,
+        )
+
+        self.assertEqual(Redirect.objects.count(), 0)
+
+    def test_offset_parameter(self):
+        invalid_file = tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8")
+        invalid_file.write("from,to\n")
+        invalid_file.write("/one,http://one.test/\n")
+        invalid_file.write("/two,http://two.test/\n")
+        invalid_file.write("/three,http://three.test/\n")
+        invalid_file.write("/four,http://four.test/")
+        invalid_file.seek(0)
+
+        out = StringIO()
+        call_command(
+            "import_redirects",
+            src=invalid_file.name,
+            format="csv",
+            offset=2,
+            stdout=out,
+        )
+
+        redirects = Redirect.objects.all()
+
+        self.assertEqual(len(redirects), 2)
+        self.assertEqual(redirects[0].old_path, "/three")
+        self.assertEqual(redirects[0].redirect_link, "http://three.test/")
+        self.assertEqual(redirects[0].is_permanent, True)
+        self.assertEqual(redirects[1].old_path, "/four")
+        self.assertEqual(redirects[1].redirect_link, "http://four.test/")
+
+    def test_limit_parameter(self):
+        invalid_file = tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8")
+        invalid_file.write("from,to\n")
+        invalid_file.write("/one,http://one.test/\n")
+        invalid_file.write("/two,http://two.test/\n")
+        invalid_file.write("/three,http://three.test/\n")
+        invalid_file.write("/four,http://four.test/")
+        invalid_file.seek(0)
+
+        out = StringIO()
+        call_command(
+            "import_redirects",
+            src=invalid_file.name,
+            format="csv",
+            limit=1,
+            stdout=out,
+        )
+
+        redirects = Redirect.objects.all()
+
+        self.assertEqual(len(redirects), 1)
+        self.assertEqual(redirects[0].old_path, "/one")
+        self.assertEqual(redirects[0].redirect_link, "http://one.test/")
+        self.assertEqual(redirects[0].is_permanent, True)
diff --git a/wagtail/contrib/redirects/tests/test_import_forms.py b/wagtail/contrib/redirects/tests/test_import_forms.py
new file mode 100644
index 0000000000..a8b2ca3031
--- /dev/null
+++ b/wagtail/contrib/redirects/tests/test_import_forms.py
@@ -0,0 +1,23 @@
+from django.test import TestCase
+
+from wagtail.contrib.redirects.forms import ConfirmImportForm, ImportForm
+
+
+class TestImportForm(TestCase):
+    def test_file_input_generates_proper_accept(self):
+        form = ImportForm(["csv", "tsv"])
+        self.assertIn('accept=".csv,.tsv"', form.as_table())
+
+
+class TestConfirmImportForm(TestCase):
+    def test_choices_get_appended_with_intro_label_if_multiple(self):
+        form = ConfirmImportForm(headers=[(0, "From"), (1, "To")])
+        first_choice = form.fields["from_index"].choices[0]
+        self.assertEqual(first_choice[0], "")
+        self.assertEqual(first_choice[1], "---")
+
+    def test_choices_does_not_get_generated_label_if_single_choice(self):
+        form = ConfirmImportForm(headers=[(1, "Hi")])
+        first_choice = form.fields["from_index"].choices[0]
+        self.assertNotEqual(first_choice[0], "")
+        self.assertNotEqual(first_choice[1], "---")
diff --git a/wagtail/contrib/redirects/tests/test_import_utils.py b/wagtail/contrib/redirects/tests/test_import_utils.py
new file mode 100644
index 0000000000..b33131e0d8
--- /dev/null
+++ b/wagtail/contrib/redirects/tests/test_import_utils.py
@@ -0,0 +1,59 @@
+import hashlib
+import os
+
+from django.core.files.base import ContentFile
+from django.test import TestCase, override_settings
+
+from wagtail.contrib.redirects.utils import (
+    get_file_storage, get_import_formats, write_to_file_storage
+)
+
+
+TEST_ROOT = os.path.abspath(os.path.dirname(__file__))
+
+
+class TestImportUtils(TestCase):
+    def test_writing_file_with_format(self):
+        f = "{}/files/example.csv".format(TEST_ROOT)
+        (_, filename) = os.path.split(f)
+
+        with open(f, "rb") as infile:
+            content_orig = infile.read()
+            upload_file = ContentFile(content_orig)
+
+            import_formats = get_import_formats()
+            import_formats = [
+                x for x in import_formats if x.__name__ == "CSV"
+            ]
+            input_format = import_formats[0]()
+            file_storage = write_to_file_storage(upload_file, input_format)
+
+            self.assertEqual(type(file_storage).__name__, "TempFolderStorage")
+
+            file_orig_checksum = hashlib.md5()
+            file_orig_checksum.update(content_orig)
+            file_orig_checksum = file_orig_checksum.hexdigest()
+
+            file_new_checksum = hashlib.md5()
+            with open(file_storage.get_full_path(), "rb") as file_new:
+                file_new_checksum.update(file_new.read())
+            file_new_checksum = file_new_checksum.hexdigest()
+
+            self.assertEqual(file_orig_checksum, file_new_checksum)
+
+    @override_settings(WAGTAIL_REDIRECTS_FILE_STORAGE='cache')
+    def test_that_cache_storage_are_returned(self):
+        FileStorage = get_file_storage()
+        self.assertEqual(FileStorage.__name__, "RedirectsCacheStorage")
+
+    def test_that_temp_folder_storage_are_returned_as_default(self):
+        FileStorage = get_file_storage()
+        self.assertEqual(FileStorage.__name__, "TempFolderStorage")
+
+    @override_settings(WAGTAIL_REDIRECTS_FILE_STORAGE='INVALID')
+    def test_invalid_file_storage_raises_errors(self):
+        with self.assertRaisesMessage(
+            Exception,
+            "Invalid file storage, must be either 'tmp_file' or 'cache'"
+        ):
+            get_file_storage()
diff --git a/wagtail/contrib/redirects/tests.py b/wagtail/contrib/redirects/tests/test_redirects.py
similarity index 100%
rename from wagtail/contrib/redirects/tests.py
rename to wagtail/contrib/redirects/tests/test_redirects.py
diff --git a/wagtail/contrib/redirects/tmp_storages.py b/wagtail/contrib/redirects/tmp_storages.py
new file mode 100644
index 0000000000..3741bb9e35
--- /dev/null
+++ b/wagtail/contrib/redirects/tmp_storages.py
@@ -0,0 +1,117 @@
+"""
+Copyright (c) Bojan Mihelac and individual contributors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    1. Redistributions of source code must retain the above copyright notice,
+       this list of conditions and the following disclaimer.
+
+    2. Redistributions in binary form must reproduce the above copyright
+       notice, this list of conditions and the following disclaimer in the
+       documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+# Copied from: https://raw.githubusercontent.com/django-import-export/django-import-export/5795e114210adf250ac6e146db2fa413f38875de/import_export/tmp_storages.py
+import os
+import tempfile
+from uuid import uuid4
+
+from django.core.cache import cache
+from django.core.files.base import ContentFile
+from django.core.files.storage import default_storage
+
+
+class BaseStorage:
+
+    def __init__(self, name=None):
+        self.name = name
+
+    def save(self, data, mode='w'):
+        raise NotImplementedError
+
+    def read(self, read_mode='r'):
+        raise NotImplementedError
+
+    def remove(self):
+        raise NotImplementedError
+
+
+class TempFolderStorage(BaseStorage):
+
+    def open(self, mode='r'):
+        if self.name:
+            return open(self.get_full_path(), mode)
+        else:
+            tmp_file = tempfile.NamedTemporaryFile(delete=False)
+            self.name = tmp_file.name
+            return tmp_file
+
+    def save(self, data, mode='w'):
+        with self.open(mode=mode) as file:
+            file.write(data)
+
+    def read(self, mode='r'):
+        with self.open(mode=mode) as file:
+            return file.read()
+
+    def remove(self):
+        os.remove(self.get_full_path())
+
+    def get_full_path(self):
+        return os.path.join(
+            tempfile.gettempdir(),
+            self.name
+        )
+
+
+class CacheStorage(BaseStorage):
+    """
+    By default memcache maximum size per key is 1MB, be careful with large files.
+    """
+    CACHE_LIFETIME = 86400
+    CACHE_PREFIX = 'django-import-export-'
+
+    def save(self, data, mode=None):
+        if not self.name:
+            self.name = uuid4().hex
+        cache.set(self.CACHE_PREFIX + self.name, data, self.CACHE_LIFETIME)
+
+    def read(self, read_mode='r'):
+        return cache.get(self.CACHE_PREFIX + self.name)
+
+    def remove(self):
+        cache.delete(self.name)
+
+
+class MediaStorage(BaseStorage):
+    MEDIA_FOLDER = 'django-import-export'
+
+    def save(self, data, mode=None):
+        if not self.name:
+            self.name = uuid4().hex
+        default_storage.save(self.get_full_path(), ContentFile(data))
+
+    def read(self, read_mode='rb'):
+        with default_storage.open(self.get_full_path(), mode=read_mode) as f:
+            return f.read()
+
+    def remove(self):
+        default_storage.delete(self.get_full_path())
+
+    def get_full_path(self):
+        return os.path.join(
+            self.MEDIA_FOLDER,
+            self.name
+        )
diff --git a/wagtail/contrib/redirects/urls.py b/wagtail/contrib/redirects/urls.py
index e6a90223e9..0ff65e943d 100644
--- a/wagtail/contrib/redirects/urls.py
+++ b/wagtail/contrib/redirects/urls.py
@@ -8,4 +8,6 @@ urlpatterns = [
     url(r'^add/$', views.add, name='add'),
     url(r'^(\d+)/$', views.edit, name='edit'),
     url(r'^(\d+)/delete/$', views.delete, name='delete'),
+    url(r"^import/$", views.start_import, name="start_import"),
+    url(r"^import/process/$", views.process_import, name="process_import"),
 ]
diff --git a/wagtail/contrib/redirects/utils.py b/wagtail/contrib/redirects/utils.py
new file mode 100644
index 0000000000..d41384bcea
--- /dev/null
+++ b/wagtail/contrib/redirects/utils.py
@@ -0,0 +1,54 @@
+from django.conf import settings
+
+from wagtail.contrib.redirects.base_formats import DEFAULT_FORMATS
+from wagtail.contrib.redirects.tmp_storages import CacheStorage, TempFolderStorage
+
+
+def write_to_file_storage(import_file, input_format):
+    FileStorage = get_file_storage()
+    file_storage = FileStorage()
+
+    data = bytes()
+    for chunk in import_file.chunks():
+        data += chunk
+
+    file_storage.save(data, input_format.get_read_mode())
+    return file_storage
+
+
+def get_supported_extensions():
+    return ("csv", "tsv", "xls", "xlsx")
+
+
+def get_format_cls_by_extension(extension):
+    formats = get_import_formats()
+
+    available_formats = [x for x in formats if x.__name__ == extension.upper()]
+
+    if not available_formats:
+        return None
+
+    return available_formats[0]
+
+
+def get_import_formats():
+    formats = [f for f in DEFAULT_FORMATS if f().can_import()]
+    return formats
+
+
+def get_file_storage():
+    file_storage = getattr(
+        settings, 'WAGTAIL_REDIRECTS_FILE_STORAGE', 'tmp_file'
+    )
+    if file_storage == 'tmp_file':
+        return TempFolderStorage
+    if file_storage == 'cache':
+        return RedirectsCacheStorage
+
+    raise Exception(
+        "Invalid file storage, must be either 'tmp_file' or 'cache'"
+    )
+
+
+class RedirectsCacheStorage(CacheStorage):
+    CACHE_PREFIX = 'wagtail-redirects-'
diff --git a/wagtail/contrib/redirects/views.py b/wagtail/contrib/redirects/views.py
index 16ee65dc99..0f16e1352e 100644
--- a/wagtail/contrib/redirects/views.py
+++ b/wagtail/contrib/redirects/views.py
@@ -1,17 +1,26 @@
+import os
+
 from django.core.paginator import Paginator
 from django.db.models import Q
-from django.shortcuts import get_object_or_404, redirect
+from django.shortcuts import get_object_or_404, redirect, render
 from django.template.response import TemplateResponse
 from django.urls import reverse
+from django.utils.encoding import force_str
 from django.utils.translation import gettext as _
+from django.utils.translation import ngettext
+from django.views.decorators.http import require_http_methods
 from django.views.decorators.vary import vary_on_headers
 
 from wagtail.admin import messages
 from wagtail.admin.auth import PermissionPolicyChecker, permission_denied
 from wagtail.admin.forms.search import SearchForm
 from wagtail.contrib.redirects import models
-from wagtail.contrib.redirects.forms import RedirectForm
+from wagtail.contrib.redirects.base_formats import DEFAULT_FORMATS
+from wagtail.contrib.redirects.forms import ConfirmImportForm, ImportForm, RedirectForm
 from wagtail.contrib.redirects.permissions import permission_policy
+from wagtail.contrib.redirects.utils import (
+    get_file_storage, get_format_cls_by_extension, get_import_formats, get_supported_extensions,
+    write_to_file_storage)
 
 permission_checker = PermissionPolicyChecker(permission_policy)
 
@@ -126,3 +135,215 @@ def add(request):
     return TemplateResponse(request, "wagtailredirects/add.html", {
         'form': form,
     })
+
+
+@permission_checker.require_any("add")
+def start_import(request):
+    supported_extensions = get_supported_extensions()
+    from_encoding = "utf-8"
+
+    query_string = request.GET.get('q', "")
+
+    if request.POST or request.FILES:
+        form_kwargs = {}
+        form = ImportForm(
+            supported_extensions,
+            request.POST or None,
+            request.FILES or None,
+            **form_kwargs
+        )
+    else:
+        form = ImportForm(supported_extensions)
+
+    if not request.FILES or not form.is_valid():
+        return render(
+            request,
+            "wagtailredirects/choose_import_file.html",
+            {
+                'search_form': SearchForm(
+                    data=dict(q=query_string) if query_string else None, placeholder=_("Search redirects")
+                ),
+                "form": form,
+            },
+        )
+
+    import_file = form.cleaned_data["import_file"]
+
+    _name, extension = os.path.splitext(import_file.name)
+    extension = extension.lstrip(".")
+
+    if extension not in supported_extensions:
+        messages.error(
+            request,
+            _('File format of type "{}" is not supported'.format(extension))
+        )
+        return redirect('wagtailredirects:start_import')
+
+    import_format_cls = get_format_cls_by_extension(extension)
+    input_format = import_format_cls()
+    file_storage = write_to_file_storage(import_file, input_format)
+
+    try:
+        data = file_storage.read(input_format.get_read_mode())
+        if not input_format.is_binary() and from_encoding:
+            data = force_str(data, from_encoding)
+        dataset = input_format.create_dataset(data)
+    except UnicodeDecodeError as e:
+        messages.error(
+            request,
+            _(u"Imported file has a wrong encoding: %s" % e)
+        )
+        return redirect('wagtailredirects:start_import')
+    except Exception as e:  # pragma: no cover
+        messages.error(
+            request,
+            _(
+                u"%s encountered while trying to read file: %s"
+                % (type(e).__name__, import_file.name)
+            )
+        )
+        return redirect('wagtailredirects:start_import')
+
+    initial = {
+        "import_file_name": file_storage.name,
+        "original_file_name": import_file.name,
+        "input_format": get_import_formats().index(import_format_cls),
+    }
+
+    return render(
+        request,
+        "wagtailredirects/confirm_import.html",
+        {
+            "form": ConfirmImportForm(dataset.headers, initial=initial),
+            "dataset": dataset,
+        },
+    )
+
+
+@permission_checker.require_any("add")
+@require_http_methods(["POST"])
+def process_import(request):
+    supported_extensions = get_supported_extensions()
+    from_encoding = "utf-8"
+
+    form_kwargs = {}
+    form = ConfirmImportForm(
+        DEFAULT_FORMATS, request.POST or None, request.FILES or None, **form_kwargs
+    )
+
+    is_confirm_form_valid = form.is_valid()
+
+    import_formats = get_import_formats()
+    input_format = import_formats[int(form.cleaned_data["input_format"])]()
+
+    FileStorage = get_file_storage()
+    file_storage = FileStorage(name=form.cleaned_data["import_file_name"])
+
+    if not is_confirm_form_valid:
+        data = file_storage.read(input_format.get_read_mode())
+        if not input_format.is_binary() and from_encoding:
+            data = force_str(data, from_encoding)
+        dataset = input_format.create_dataset(data)
+
+        initial = {
+            "import_file_name": file_storage.name,
+            "original_file_name": form.cleaned_data["import_file_name"],
+        }
+
+        return render(
+            request,
+            "wagtailredirects/confirm_import.html",
+            {
+                "form": ConfirmImportForm(
+                    dataset.headers,
+                    request.POST or None,
+                    request.FILES or None,
+                    initial=initial,
+                ),
+                "dataset": dataset,
+            },
+        )
+
+    data = file_storage.read(input_format.get_read_mode())
+    if not input_format.is_binary() and from_encoding:
+        data = force_str(data, from_encoding)
+    dataset = input_format.create_dataset(data)
+
+    import_summary = create_redirects_from_dataset(
+        dataset,
+        {
+            "from_index": int(form.cleaned_data["from_index"]),
+            "to_index": int(form.cleaned_data["to_index"]),
+            "permanent": form.cleaned_data["permanent"],
+            "site": form.cleaned_data["site"],
+        },
+    )
+
+    file_storage.remove()
+
+    if import_summary["errors_count"] > 0:
+        return render(
+            request,
+            "wagtailredirects/import_summary.html",
+            {
+                "form": ImportForm(supported_extensions),
+                "import_summary": import_summary,
+            },
+        )
+
+    total = import_summary["total"]
+    messages.success(
+        request,
+        ngettext(
+            "Imported %(total)d redirect",
+            "Imported %(total)d redirects",
+            total
+        ) % {'total': total}
+    )
+
+    return redirect('wagtailredirects:index')
+
+
+def create_redirects_from_dataset(dataset, config):
+    errors = []
+    successes = 0
+    total = 0
+
+    for row in dataset:
+        total += 1
+
+        from_link = row[config["from_index"]]
+        to_link = row[config["to_index"]]
+
+        data = {
+            "old_path": from_link,
+            "redirect_link": to_link,
+            "is_permanent": config["permanent"],
+        }
+
+        if config["site"]:
+            data["site"] = config["site"].pk
+
+        form = RedirectForm(data)
+        if not form.is_valid():
+            error = to_readable_errors(form.errors.as_text())
+            errors.append([from_link, to_link, error])
+            continue
+
+        form.save()
+        successes += 1
+
+    return {
+        "errors": errors,
+        "errors_count": len(errors),
+        "successes": successes,
+        "total": total,
+    }
+
+
+def to_readable_errors(error):
+    errors = error.split("\n")
+    errors = errors[1::2]
+    errors = [x.lstrip('* ') for x in errors]
+    errors = ", ".join(errors)
+    return errors