Skip to content

Commit

Permalink
Merge pull request #21 from tangochin/master
Browse files Browse the repository at this point in the history
Revert support for Django 1.10 and Django 1.11, replaced django.contrib.redirects for SEO-redirects with subdomain support
  • Loading branch information
varche1 committed Oct 31, 2018
2 parents 96c1e76 + 9df77a0 commit 9c39fda
Show file tree
Hide file tree
Showing 11 changed files with 224 additions and 70 deletions.
8 changes: 8 additions & 0 deletions .travis.yml
@@ -1,11 +1,19 @@
language: python
python:
- "3.4"
- "3.5"
- "3.6"
env:
- DJANGO=1.10 DB=sqlite
- DJANGO=1.11 DB=sqlite
- DJANGO=2.0 DB=sqlite
- DJANGO=2.1 DB=sqlite
matrix:
exclude:
- python: "3.4"
env: DJANGO=2.1 DB=sqlite
- python: "3.5"
env: DJANGO=2.1 DB=sqlite
install:
- pip install -q Django==$DJANGO
- pip install coveralls coverage django-discover-runner
Expand Down
3 changes: 2 additions & 1 deletion README.rst
Expand Up @@ -244,7 +244,6 @@ Redirects
---------

Currently supported are two types of redirects: when an occurs error 404 and when model changes its URL on the site.
For each type of redirects used functional of `django.contrib.redirects <https://docs.djangoproject.com/en/1.10/ref/contrib/redirects/>`_. You must configure it before use redirects from ``django-seo``.

If you need a redirection when an error occurs 404, enable ``SEO_USE_REDIRECTS`` and setup URL patterns for redirection in admin interface.
It's like a standard URL patterns, but instead of finding a suitable view it creates a redirect in case of an error 404 for a given pattern.
Expand All @@ -259,6 +258,8 @@ If you need a redirection when model changes its URL list the full path to the m
'your_app.models.Bar'
)
The default class for redirection is ``django.http.response.HttpResponsePermanentRedirect``, but if you want to change this behavior, you can change the HttpResponse classes used by the middleware by creating a subclass of RedirectFallbackMiddleware and overriding response_redirect_class.

Attention: each path to model must be direct and model must have a method ``get_absolute_url``.
Work such redirection follows: when path to model on site changed, it create redirection to old path.
For example:
Expand Down
5 changes: 3 additions & 2 deletions djangoseo/base.py
Expand Up @@ -369,7 +369,7 @@ def _handle_redirects_callback(model_class, sender, instance, **kwargs):
create instances of redirects for changed URLs.
"""
# avoid RuntimeError for apps without enabled redirects
from django.contrib.redirects.models import Redirect
from .models import Redirect

if not instance.pk:
return
Expand All @@ -380,7 +380,8 @@ def _handle_redirects_callback(model_class, sender, instance, **kwargs):
Redirect.objects.get_or_create(
old_path=before,
new_path=after,
site=Site.objects.get_current()
site=Site.objects.get_current(),
all_subdomains=True
)
except Exception as e:
logger.exception('Failed to create new redirect')
Expand Down
75 changes: 71 additions & 4 deletions djangoseo/middleware.py
@@ -1,22 +1,89 @@
import re
from logging import getLogger

from django import http
from django.apps import apps
from django.db.models import Q
from django.conf import settings
from django.http import Http404
from django.utils.deprecation import MiddlewareMixin
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import ImproperlyConfigured

from .models import Redirect
from .utils import handle_seo_redirects


logger = getLogger(__name__)


# TODO: replace after removing support for old versions of Django
class MiddlewareMixin(object):
"""
This mixin a full copy of Django 1.10 django.utils.deprecation.MiddlewareMixin.
Needed for compatibility reasons.
"""
def __init__(self, get_response=None):
self.get_response = get_response
super(MiddlewareMixin, self).__init__()

def __call__(self, request):
response = None
if hasattr(self, 'process_request'):
response = self.process_request(request)
if not response:
response = self.get_response(request)
if hasattr(self, 'process_response'):
response = self.process_response(request, response)
return response


class RedirectsMiddleware(MiddlewareMixin):

def process_exception(self, request, exception):
if not getattr(settings, 'SEO_USE_REDIRECTS', False):
return

if request.method == 'GET' and isinstance(exception, Http404):
if request.method == 'GET' and isinstance(exception, http.Http404):
handle_seo_redirects(request)
return


class RedirectFallbackMiddleware(MiddlewareMixin):
# Defined as class-level attributes to be subclassing-friendly.
response_gone_class = http.HttpResponseGone
response_redirect_class = http.HttpResponsePermanentRedirect

def __init__(self, get_response=None):
if not apps.is_installed('django.contrib.sites'):
raise ImproperlyConfigured(
'You cannot use RedirectFallbackMiddleware when '
'django.contrib.sites is not installed.'
)
super(RedirectFallbackMiddleware, self).__init__(get_response)

def process_response(self, request, response):
# No need to check for a redirect for non-404 responses.
if response.status_code != 404:
return response

subdomain = getattr(request, 'subdomain', '')
full_path = request.get_full_path()
current_site = get_current_site(request)

redirect = Redirect.objects.filter(
Q(site=current_site),
Q(old_path=full_path),
Q(subdomain=subdomain) | Q(all_subdomains=True)
).order_by('all_subdomains').first()

if redirect is None and settings.APPEND_SLASH and not request.path.endswith('/'):
redirect = Redirect.objects.filter(
Q(site=current_site),
Q(old_path=request.get_full_path(force_append_slash=True)),
Q(subdomain=subdomain) | Q(all_subdomains=True)
).order_by('all_subdomains').first()
if redirect is not None:
if redirect.new_path == '':
return self.response_gone_class()
return self.response_redirect_class(redirect.new_path)

# No redirect was found. Return the response.
return response
66 changes: 61 additions & 5 deletions djangoseo/models.py
Expand Up @@ -11,10 +11,12 @@


RedirectPattern = None
Redirect = None


def setup():
global RedirectPattern
global Redirect
is_loaded = False
# Look for Metadata subclasses in appname/seo.py files
for app in settings.INSTALLED_APPS:
Expand All @@ -31,10 +33,10 @@ def setup():

# if SEO_USE_REDIRECTS is enabled, add model for redirect and register it in admin
if getattr(settings, 'SEO_USE_REDIRECTS', False):
def magic_str_method(self):
return self.redirect_path
def redirect_pattern_str_method(self):
return '%s -> %s' % (self.url_pattern, self.redirect_path)

class Meta:
class RedirectPatternMeta:
verbose_name = _('Redirect pattern')
verbose_name_plural = _('Redirect patterns')

Expand Down Expand Up @@ -64,8 +66,8 @@ class Meta:
default=False,
help_text=_('Pattern works for all subdomains')
),
'__str__': magic_str_method,
'Meta': Meta
'__str__': redirect_pattern_str_method,
'Meta': RedirectPatternMeta
})

RedirectPatternAdmin = type('RedirectPatternAdmin', (admin.ModelAdmin,), {
Expand All @@ -75,7 +77,61 @@ class Meta:
'search_fields': ['redirect_path'],
})

class RedirectMeta:
verbose_name = _('redirect')
verbose_name_plural = _('redirects')
unique_together = (('site', 'old_path'),)
ordering = ('old_path',)

def redirect_str_method(self):
return '%s -> %s' % (self.old_path, self.new_path)

from django.contrib.sites.models import Site

Redirect = create_dynamic_model('Redirect', **{
'site': models.ForeignKey(
to=Site,
on_delete=models.CASCADE,
verbose_name=_('site'),
related_name='seo_redirect'
),
'old_path': models.CharField(
verbose_name=_('redirect from'),
max_length=200,
db_index=True,
help_text=_("This should be an absolute path, excluding the domain name. Example: '/events/search/'."),
),
'new_path': models.CharField(
verbose_name=_('redirect to'),
max_length=200,
blank=True,
help_text=_("This can be either an absolute path (as above) or a full URL starting with 'http://'."),
),
'subdomain': models.CharField(
verbose_name=_('subdomain'),
max_length=250,
blank=True,
null=True,
default=''
),
'all_subdomains': models.BooleanField(
verbose_name=_('all subdomains'),
default=False,
help_text=_('Will works for all subdomains')
),
'__str__': redirect_str_method,
'Meta': RedirectMeta
})

RedirectAdmin = type('RedirectAdmin', (admin.ModelAdmin,), {
'list_display': ('old_path', 'new_path'),
'list_filter': ('site',),
'search_fields': ('old_path', 'new_path'),
'radio_fields': {'site': admin.VERTICAL}
})

register_model_in_admin(RedirectPattern, RedirectPatternAdmin)
register_model_in_admin(Redirect, RedirectAdmin)

from djangoseo.base import register_signals
register_signals()
49 changes: 33 additions & 16 deletions djangoseo/utils.py
Expand Up @@ -3,24 +3,28 @@
import re
import importlib

import django
from django.contrib.sites.shortcuts import get_current_site
from django.utils.functional import lazy
from django.utils.safestring import mark_safe
from django.utils.module_loading import import_string
from django.utils.html import conditional_escape
from django.urls import (URLResolver as RegexURLResolver, URLPattern as RegexURLPattern, Resolver404, get_resolver,
clear_url_caches)
from django.conf import settings
from django.db import models
from django.db.models import Q
from django.utils import six

if django.VERSION < (2, 0):
from django.core.urlresolvers import RegexURLResolver, RegexURLPattern, Resolver404, get_resolver, clear_url_caches
else:
from django.urls import (URLResolver as RegexURLResolver, URLPattern as RegexURLPattern, Resolver404, get_resolver,
clear_url_caches)

logger = logging.getLogger(__name__)


class NotSet(object):
""" A singleton to identify unset values (where None would have meaning) """

def __str__(self):
return "NotSet"

Expand All @@ -33,12 +37,16 @@ def __repr__(self):

class Literal(object):
""" Wrap literal values so that the system knows to treat them that way """

def __init__(self, value):
self.value = value


def _pattern_resolve_to_name(pattern, path):
match = pattern.pattern.regex.search(path)
if django.VERSION < (2, 0):
match = pattern.regex.search(path)
else:
match = pattern.pattern.regex.search(path)
if match:
name = ""
if pattern.name:
Expand All @@ -52,7 +60,11 @@ def _pattern_resolve_to_name(pattern, path):

def _resolver_resolve_to_name(resolver, path):
tried = []
match = resolver.pattern.regex.search(path)
django1 = django.VERSION < (2, 0)
if django1:
match = resolver.regex.search(path)
else:
match = resolver.pattern.regex.search(path)
if match:
new_path = path[match.end():]
for pattern in resolver.url_patterns:
Expand All @@ -62,11 +74,17 @@ def _resolver_resolve_to_name(resolver, path):
elif isinstance(pattern, RegexURLResolver):
name = _resolver_resolve_to_name(pattern, new_path)
except Resolver404 as e:
tried.extend([(pattern.pattern.regex.pattern + ' ' + t) for t in e.args[0]['tried']])
if django1:
tried.extend([(pattern.regex.pattern + ' ' + t) for t in e.args[0]['tried']])
else:
tried.extend([(pattern.pattern.regex.pattern + ' ' + t) for t in e.args[0]['tried']])
else:
if name:
return name
tried.append(pattern.pattern.regex.pattern)
if django1:
tried.append(pattern.regex.pattern)
else:
tried.append(pattern.pattern.regex.pattern)
raise Resolver404({'tried': tried, 'path': new_path})


Expand All @@ -84,12 +102,10 @@ def _replace_quot(match):

def escape_tags(value, valid_tags):
""" Strips text from the given html string, leaving only tags.
This functionality requires BeautifulSoup, nothing will be
This functionality requires BeautifulSoup, nothing will be
done otherwise.
This isn't perfect. Someone could put javascript in here:
<a onClick="alert('hi');">test</a>
So if you use valid_tags, you still need to trust your data entry.
Or we could try:
- only escape the non matching bits
Expand All @@ -111,7 +127,7 @@ def escape_tags(value, valid_tags):

# Allow comments to be hidden
value = value.replace("&lt;!--", "<!--").replace("--&gt;", "-->")

return mark_safe(value)


Expand All @@ -122,7 +138,7 @@ def _get_seo_content_types(seo_models):
from django.contrib.contenttypes.models import ContentType
try:
return [ContentType.objects.get_for_model(m).id for m in seo_models]
except: # previously caught DatabaseError
except: # previously caught DatabaseError
# Return an empty list if this is called too early
return []

Expand Down Expand Up @@ -185,11 +201,10 @@ def import_tracked_models():

def handle_seo_redirects(request):
"""
Handle SEO redirects. Create django.contrib.redirects.models.Redirect if exists redirect pattern.
Handle SEO redirects. Create Redirect instance if exists redirect pattern.
:param request: Django request
"""
from .models import RedirectPattern
from django.contrib.redirects.models import Redirect
from .models import RedirectPattern, Redirect

if not getattr(settings, 'SEO_USE_REDIRECTS', False):
return
Expand All @@ -208,7 +223,9 @@ def handle_seo_redirects(request):
kwargs = {
'site': current_site,
'old_path': full_path,
'new_path': redirect_pattern.redirect_path
'new_path': redirect_pattern.redirect_path,
'subdomain': redirect_pattern.subdomain,
'all_subdomains': redirect_pattern.all_subdomains
}
try:
Redirect.objects.get_or_create(**kwargs)
Expand Down
2 changes: 1 addition & 1 deletion djangoseo/version.py
@@ -1,2 +1,2 @@
# -*- coding:utf-8 -*-
__version__ = "2.5.0+whyfly.1"
__version__ = '2.6.0+whyfly.1'

0 comments on commit 9c39fda

Please sign in to comment.