diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/shared/main_nav.html b/wagtail/wagtailadmin/templates/wagtailadmin/shared/main_nav.html
index 4e4553ba0..234e1118b 100644
--- a/wagtail/wagtailadmin/templates/wagtailadmin/shared/main_nav.html
+++ b/wagtail/wagtailadmin/templates/wagtailadmin/shared/main_nav.html
@@ -19,7 +19,7 @@
More
-
+
{% get_wagtailadmin_tab_urls as wagtailadmin_tab_urls %}
{% for name, title in wagtailadmin_tab_urls %}
diff --git a/wagtail/wagtailredirects/__init__.py b/wagtail/wagtailredirects/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/wagtail/wagtailredirects/forms.py b/wagtail/wagtailredirects/forms.py
new file mode 100644
index 000000000..2adfab830
--- /dev/null
+++ b/wagtail/wagtailredirects/forms.py
@@ -0,0 +1,8 @@
+from django import forms
+
+import models
+
+
+class RedirectForm(forms.ModelForm):
+ class Meta:
+ model = models.Redirect
\ No newline at end of file
diff --git a/wagtail/wagtailredirects/middleware.py b/wagtail/wagtailredirects/middleware.py
new file mode 100644
index 000000000..d2997427f
--- /dev/null
+++ b/wagtail/wagtailredirects/middleware.py
@@ -0,0 +1,27 @@
+from django import http
+
+import models
+
+
+# Originally pinched from: https://github.com/django/django/blob/master/django/contrib/redirects/middleware.py
+class RedirectMiddleware(object):
+ def process_response(self, request, response):
+ # No need to check for a redirect for non-404 responses.
+ if response.status_code != 404:
+ return response
+
+ # Get the path
+ path = models.Redirect.normalise_path(request.get_full_path())
+
+ # Find redirect
+ try:
+ redirect = models.Redirect.get_for_site(request.site).get(old_path=path)
+
+ if redirect.is_permanent:
+ return http.HttpResponsePermanentRedirect(redirect.link)
+ else:
+ return http.HttpResponseTemporaryRedirect(redirect.link)
+ except:
+ pass
+
+ return response
\ No newline at end of file
diff --git a/wagtail/wagtailredirects/migrations/0001_initial.py b/wagtail/wagtailredirects/migrations/0001_initial.py
new file mode 100644
index 000000000..b60023bfa
--- /dev/null
+++ b/wagtail/wagtailredirects/migrations/0001_initial.py
@@ -0,0 +1,106 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ depends_on = (
+ ("wagtailcore", "0002_initial_data"),
+ )
+
+ def forwards(self, orm):
+ # Adding model 'Redirect'
+ db.create_table(u'wagtailredirects_redirect', (
+ (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('old_path', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255, db_index=True)),
+ ('site', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='redirects', null=True, to=orm['wagtailcore.Site'])),
+ ('is_permanent', self.gf('django.db.models.fields.BooleanField')(default=True)),
+ ('redirect_page', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='+', null=True, to=orm['wagtailcore.Page'])),
+ ('redirect_link', self.gf('django.db.models.fields.URLField')(max_length=200, blank=True)),
+ ))
+ db.send_create_signal(u'wagtailredirects', ['Redirect'])
+
+
+ def backwards(self, orm):
+ # Deleting model 'Redirect'
+ db.delete_table(u'wagtailredirects_redirect')
+
+
+ models = {
+ u'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ u'wagtailcore.page': {
+ 'Meta': {'object_name': 'Page'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': u"orm['contenttypes.ContentType']"}),
+ 'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'feed_image': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': u"orm['rca.RcaImage']"}),
+ 'has_unpublished_changes': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'live': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'seo_title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+ 'show_in_menus': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'url_path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'})
+ },
+ u'wagtailcore.site': {
+ 'Meta': {'object_name': 'Site'},
+ 'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_default_site': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'port': ('django.db.models.fields.IntegerField', [], {'default': '80'}),
+ 'root_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sites_rooted_here'", 'to': u"orm['wagtailcore.Page']"})
+ },
+ u'rca.rcaimage': {
+ 'Meta': {'object_name': 'RcaImage'},
+ 'alt': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'creator': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+ 'dimensions': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+ 'eprint_docid': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+ 'file': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}),
+ 'height': ('django.db.models.fields.IntegerField', [], {}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'medium': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+ 'permission': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+ 'photographer': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+ 'rca_content_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'width': ('django.db.models.fields.IntegerField', [], {}),
+ 'year': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'})
+ },
+ u'taggit.tag': {
+ 'Meta': {'object_name': 'Tag'},
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'})
+ },
+ u'taggit.taggeditem': {
+ 'Meta': {'object_name': 'TaggedItem'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_tagged_items'", 'to': u"orm['contenttypes.ContentType']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_items'", 'to': u"orm['taggit.Tag']"})
+ },
+ u'wagtailredirects.redirect': {
+ 'Meta': {'object_name': 'Redirect'},
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_permanent': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'old_path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
+ 'redirect_link': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
+ 'redirect_page': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': u"orm['wagtailcore.Page']"}),
+ 'site': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'redirects'", 'null': 'True', 'to': u"orm['wagtailcore.Site']"})
+ }
+ }
+
+ complete_apps = ['wagtailredirects']
\ No newline at end of file
diff --git a/wagtail/wagtailredirects/migrations/__init__.py b/wagtail/wagtailredirects/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/wagtail/wagtailredirects/models.py b/wagtail/wagtailredirects/models.py
new file mode 100644
index 000000000..ce6296831
--- /dev/null
+++ b/wagtail/wagtailredirects/models.py
@@ -0,0 +1,72 @@
+from django.db import models
+from wagtail.wagtailadmin.edit_handlers import FieldPanel, MultiFieldPanel, PageChooserPanel
+
+
+class Redirect(models.Model):
+ old_path = models.CharField("Redirect from",max_length=255, unique=True, db_index=True)
+ site = models.ForeignKey('wagtailcore.Site', null=True, blank=True, related_name='redirects', db_index=True, editable=False)
+ is_permanent = models.BooleanField("Permanent", default=True, help_text="Recommended. Permanent redirects ensure search engines forget the old page (the 'Redirect from') and index the new page instead.")
+ redirect_page = models.ForeignKey('wagtailcore.Page', verbose_name="Redirect to a page", related_name='+', null=True, blank=True)
+ redirect_link = models.URLField("Redirect to any URL", blank=True)
+
+ @property
+ def title(self):
+ return self.old_path
+
+ @property
+ def link(self):
+ if self.redirect_page:
+ return self.redirect_page.url
+ else:
+ return self.redirect_link
+
+ def get_is_permanent_display(self):
+ if self.is_permanent:
+ return "permanent"
+ else:
+ return "temporary"
+
+ @classmethod
+ def get_for_site(cls, site=None):
+ if site:
+ return cls.objects.filter(models.Q(site=site) | models.Q(site=None))
+ else:
+ return cls.objects.all()
+
+ @staticmethod
+ def normalise_path(full_path):
+ # Split full_path into path and query_string
+ try:
+ question_mark = full_path.index('?')
+ path = full_path[:question_mark]
+ query_string = full_path[question_mark:]
+ except ValueError:
+ path = full_path
+ query_string = ''
+
+ # Check that the path has content before normalising
+ if path is None or path == '':
+ return query_string
+
+ # Make sure theres a '/' at the beginning
+ if path[0] != '/':
+ path = '/' + path
+
+ # Make sure theres not a '/' at the end
+ if path[-1] == '/':
+ path = path[:-1]
+
+ return path + query_string
+
+ def clean(self):
+ # Normalise old path
+ self.old_path = Redirect.normalise_path(self.old_path)
+
+Redirect.content_panels = [
+ MultiFieldPanel([
+ FieldPanel('old_path'),
+ FieldPanel('is_permanent'),
+ PageChooserPanel('redirect_page'),
+ FieldPanel('redirect_link'),
+ ])
+]
\ No newline at end of file
diff --git a/wagtail/wagtailredirects/templates/wagtailredirects/add.html b/wagtail/wagtailredirects/templates/wagtailredirects/add.html
new file mode 100644
index 000000000..4fea72ccd
--- /dev/null
+++ b/wagtail/wagtailredirects/templates/wagtailredirects/add.html
@@ -0,0 +1,23 @@
+{% extends "wagtailadmin/base.html" %}
+{% block titletag %}Add redirect{% endblock %}
+{% block content %}
+
+
+
+{% endblock %}
+
+{% block extra_css %}
+ {% include "wagtailadmin/pages/_editor_css.html" %}
+{% endblock %}
+{% block extra_js %}
+ {% include "wagtailadmin/pages/_editor_js.html" %}
+{% endblock %}
\ No newline at end of file
diff --git a/wagtail/wagtailredirects/templates/wagtailredirects/confirm_delete.html b/wagtail/wagtailredirects/templates/wagtailredirects/confirm_delete.html
new file mode 100644
index 000000000..61b5dece2
--- /dev/null
+++ b/wagtail/wagtailredirects/templates/wagtailredirects/confirm_delete.html
@@ -0,0 +1,15 @@
+{% extends "wagtailadmin/base.html" %}
+
+{% block titletag %}Delete redirect {{ redirect.title }}{% endblock %}
+{% block content %}
+
+ Delete {{ redirect.title }}
+
+
+
Are you sure you want to delete this redirect?
+
+
+{% endblock %}
diff --git a/wagtail/wagtailredirects/templates/wagtailredirects/edit.html b/wagtail/wagtailredirects/templates/wagtailredirects/edit.html
new file mode 100644
index 000000000..44751185e
--- /dev/null
+++ b/wagtail/wagtailredirects/templates/wagtailredirects/edit.html
@@ -0,0 +1,24 @@
+{% extends "wagtailadmin/base.html" %}
+{% block titletag %}Editing {{ redirect.title }}{% endblock %}
+{% block content %}
+
+ Editing {{ redirect.title }}
+
+
+
+{% endblock %}
+
+{% block extra_css %}
+ {% include "wagtailadmin/pages/_editor_css.html" %}
+{% endblock %}
+{% block extra_js %}
+ {% include "wagtailadmin/pages/_editor_js.html" %}
+{% endblock %}
\ No newline at end of file
diff --git a/wagtail/wagtailredirects/templates/wagtailredirects/index.html b/wagtail/wagtailredirects/templates/wagtailredirects/index.html
new file mode 100644
index 000000000..2b3da2529
--- /dev/null
+++ b/wagtail/wagtailredirects/templates/wagtailredirects/index.html
@@ -0,0 +1,50 @@
+{% extends "wagtailadmin/base.html" %}
+{% block titletag %}Redirects{% endblock %}
+{% block bodyclass %}page-explorer{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+ | From |
+ To |
+ Type |
+
+
+
+ {% if redirects %}
+ {% for redirect in redirects %}
+
+ |
+
+ |
+
+ {% if redirect.redirect_page %}
+ {{ redirect.redirect_page.title }}
+ {% else %}
+ {{ redirect.link }}
+ {% endif %}
+ |
+ {{ redirect.get_is_permanent_display }} |
+
+ {% endfor %}
+ {% else %}
+ No redirects have been added. Why not add one? |
+ {% endif %}
+
+
+{% endblock %}
diff --git a/wagtail/wagtailredirects/tests.py b/wagtail/wagtailredirects/tests.py
new file mode 100644
index 000000000..501deb776
--- /dev/null
+++ b/wagtail/wagtailredirects/tests.py
@@ -0,0 +1,16 @@
+"""
+This file demonstrates writing tests using the unittest module. These will pass
+when you run "manage.py test".
+
+Replace this with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+
+class SimpleTest(TestCase):
+ def test_basic_addition(self):
+ """
+ Tests that 1 + 1 always equals 2.
+ """
+ self.assertEqual(1 + 1, 2)
diff --git a/wagtail/wagtailredirects/urls.py b/wagtail/wagtailredirects/urls.py
new file mode 100644
index 000000000..e3edb93bd
--- /dev/null
+++ b/wagtail/wagtailredirects/urls.py
@@ -0,0 +1,9 @@
+from django.conf.urls import patterns, url
+
+
+urlpatterns = patterns('wagtail.wagtailredirects.views',
+ url(r'^$', 'index', name='wagtailredirects_index'),
+ url(r'^(\d+)/$', 'edit', name='wagtailredirects_edit_redirect'),
+ url(r'^(\d+)/delete/$', 'delete', name='wagtailredirects_delete_redirect'),
+ url(r'^add/$', 'add', name='wagtailredirects_add_redirect'),
+)
diff --git a/wagtail/wagtailredirects/views.py b/wagtail/wagtailredirects/views.py
new file mode 100644
index 000000000..fa040d177
--- /dev/null
+++ b/wagtail/wagtailredirects/views.py
@@ -0,0 +1,85 @@
+from django.shortcuts import render, redirect, get_object_or_404
+from django.contrib import messages
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+from django.contrib.auth.decorators import permission_required
+from wagtail.wagtailadmin.edit_handlers import ObjectList
+
+import models
+import forms
+
+
+REDIRECT_EDIT_HANDLER = ObjectList(models.Redirect.content_panels)
+
+@permission_required('wagtailredirects.change_redirect')
+def index(request):
+ # Get redirects
+ redirects = models.Redirect.get_for_site(site=request.site)
+
+ # Render template
+ return render(request, "wagtailredirects/index.html", {
+ 'redirects': redirects,
+ })
+
+
+@permission_required('wagtailredirects.change_redirect')
+def edit(request, redirect_id):
+ theredirect = get_object_or_404(models.Redirect, id=redirect_id)
+
+ form_class = REDIRECT_EDIT_HANDLER.get_form_class(models.Redirect)
+ if request.POST:
+ form = form_class(request.POST, request.FILES, instance=theredirect)
+ if form.is_valid():
+ form.save()
+ messages.success(request, "Redirect '%s' updated." % theredirect.title)
+ return redirect('wagtailredirects_index')
+ else:
+ messages.error(request, "The redirect could not be saved due to errors.")
+ edit_handler = REDIRECT_EDIT_HANDLER(instance=theredirect, form=form)
+ else:
+ form = form_class(instance=theredirect)
+ edit_handler = REDIRECT_EDIT_HANDLER(instance=theredirect, form=form)
+
+ return render(request, "wagtailredirects/edit.html", {
+ 'redirect': theredirect,
+ 'edit_handler': edit_handler,
+ })
+
+
+@permission_required('wagtailredirects.change_redirect')
+def delete(request, redirect_id):
+ theredirect = get_object_or_404(models.Redirect, id=redirect_id)
+
+ if request.POST:
+ theredirect.delete()
+ messages.success(request, "Redirect '%s' deleted." % theredirect.title)
+ return redirect('wagtailredirects_index')
+
+ return render(request, "wagtailredirects/confirm_delete.html", {
+ 'redirect': theredirect,
+ })
+
+
+@permission_required('wagtailredirects.change_redirect')
+def add(request):
+ theredirect = models.Redirect()
+
+ form_class = REDIRECT_EDIT_HANDLER.get_form_class(models.Redirect)
+ if request.POST:
+ form = form_class(request.POST, request.FILES)
+ if form.is_valid():
+ theredirect = form.save(commit=False)
+ theredirect.site = request.site
+ theredirect.save()
+
+ messages.success(request, "Redirect '%s' added." % theredirect.title)
+ return redirect('wagtailredirects_index')
+ else:
+ messages.error(request, "The redirect could not be created due to errors.")
+ edit_handler = REDIRECT_EDIT_HANDLER(instance=theredirect, form=form)
+ else:
+ form = form_class()
+ edit_handler = REDIRECT_EDIT_HANDLER(instance=theredirect, form=form)
+
+ return render(request, "wagtailredirects/add.html", {
+ 'edit_handler': edit_handler,
+ })
\ No newline at end of file