Add generic Geo redirector view class

* Support full URLs as well as URL names
* Add docs and never cache headers
* Clean up the view docs a bit

Fix #6084
This commit is contained in:
Paul McLanahan 2019-08-27 14:52:19 -04:00 коммит произвёл Alex Gibson
Родитель 50fa411dfc
Коммит ed7b0e5a73
4 изменённых файлов: 188 добавлений и 29 удалений

Просмотреть файл

@ -3,7 +3,7 @@ import json
from django.test import TestCase, RequestFactory from django.test import TestCase, RequestFactory
from django.test import override_settings from django.test import override_settings
from bedrock.base.views import geolocate from bedrock.base.views import geolocate, GeoRedirectView
class TestGeolocate(TestCase): class TestGeolocate(TestCase):
@ -33,3 +33,53 @@ class TestGeolocate(TestCase):
def test_dev_mode(self): def test_dev_mode(self):
# should match the setting in DEV mode # should match the setting in DEV mode
self.assertDictEqual(self.get_country('US'), {'country_code': 'DE'}) self.assertDictEqual(self.get_country('US'), {'country_code': 'DE'})
geo_view = GeoRedirectView.as_view(
geo_urls={
'CA': 'firefox.new',
'US': 'firefox',
},
default_url='https://abide.dude'
)
@override_settings(DEV=False)
class TestGeoRedirectView(TestCase):
def get_response(self, country):
rf = RequestFactory()
req = rf.get('/', HTTP_CF_IPCOUNTRY=country)
return geo_view(req)
def test_special_country(self):
resp = self.get_response('CA')
assert resp.status_code == 302
assert resp['location'] == '/firefox/new/'
assert resp['cache-control'] == 'max-age=0, no-cache, no-store, must-revalidate'
resp = self.get_response('US')
assert resp.status_code == 302
assert resp['location'] == '/firefox/'
assert resp['cache-control'] == 'max-age=0, no-cache, no-store, must-revalidate'
def test_other_country(self):
resp = self.get_response('DE')
assert resp.status_code == 302
assert resp['location'] == 'https://abide.dude'
assert resp['cache-control'] == 'max-age=0, no-cache, no-store, must-revalidate'
resp = self.get_response('JA')
assert resp.status_code == 302
assert resp['location'] == 'https://abide.dude'
assert resp['cache-control'] == 'max-age=0, no-cache, no-store, must-revalidate'
def test_invalid_country(self):
resp = self.get_response('dude')
assert resp.status_code == 302
assert resp['location'] == 'https://abide.dude'
assert resp['cache-control'] == 'max-age=0, no-cache, no-store, must-revalidate'
resp = self.get_response('42')
assert resp.status_code == 302
assert resp['location'] == 'https://abide.dude'
assert resp['cache-control'] == 'max-age=0, no-cache, no-store, must-revalidate'

Просмотреть файл

@ -1,6 +1,7 @@
import json import json
import logging import logging
import os.path import os.path
import re
from datetime import datetime from datetime import datetime
from os import getenv from os import getenv
from time import time from time import time
@ -9,6 +10,8 @@ import timeago
from django.conf import settings from django.conf import settings
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
from django.shortcuts import render from django.shortcuts import render
from django.utils.decorators import method_decorator
from django.views.generic import RedirectView
from django.views.decorators.cache import never_cache from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST, require_safe from django.views.decorators.http import require_POST, require_safe
@ -31,6 +34,26 @@ def get_geo_from_request(request):
return country_code.upper() return country_code.upper()
@method_decorator(never_cache, name='dispatch')
class GeoRedirectView(RedirectView):
# dict of country codes to full URLs or URL names
geo_urls = None
# default URL or URL name for countries not in `geo_urls`
default_url = None
# default to sending the query parameters through to the redirect
query_string = True
def get_redirect_url(self, *args, **kwargs):
country_code = get_geo_from_request(self.request)
url = self.geo_urls.get(country_code, self.default_url)
if re.match(r'https?://', url, re.I):
self.url = url
else:
self.pattern_name = url
return super().get_redirect_url(*args, **kwargs)
@require_safe @require_safe
@never_cache @never_cache
def geolocate(request): def geolocate(request):

Просмотреть файл

@ -15,11 +15,15 @@ URL patterns should be as strict as possible. It should begin with a
`^` and end with `/$` to make sure it only matches what you specifiy. `^` and end with `/$` to make sure it only matches what you specifiy.
It also forces a trailing slash. You should also give the URL a name It also forces a trailing slash. You should also give the URL a name
so that other pages can reference it instead of hardcoding the URL. so that other pages can reference it instead of hardcoding the URL.
Example:: Example:
.. code-block:: python
url(r'^channel/$', channel, name='mozorg.channel') url(r'^channel/$', channel, name='mozorg.channel')
Bedrock comes with a handy shortcut to automate all of this:: Bedrock comes with a handy shortcut to automate all of this:
.. code-block:: python
from bedrock.mozorg.util import page from bedrock.mozorg.util import page
page('channel', 'mozorg/channel.html') page('channel', 'mozorg/channel.html')
@ -28,6 +32,8 @@ You don't even need to create a view. It will serve up the specified
template at the given URL (the first parameter). You can also pass template at the given URL (the first parameter). You can also pass
template data as keyword arguments: template data as keyword arguments:
.. code-block:: python
page('channel', 'mozorg/channel.html', page('channel', 'mozorg/channel.html',
latest_version=product_details.firefox_versions['LATEST_FIREFOX_VERSION']) latest_version=product_details.firefox_versions['LATEST_FIREFOX_VERSION'])
@ -62,23 +68,31 @@ Images should be included on pages using helper functions.
static() static()
^^^^^^^^ ^^^^^^^^
For a simple image, the `static()` function is used to generate the image URL. For example:: For a simple image, the `static()` function is used to generate the image URL. For example:
.. code-block:: html
<img src="{{ static('img/firefox/new/firefox-logo.png') }}" alt="Firefox" /> <img src="{{ static('img/firefox/new/firefox-logo.png') }}" alt="Firefox" />
will output an image:: will output an image:
.. code-block:: html
<img src="/media/img/firefox/new/firefox-logo.png" alt="Firefox"> <img src="/media/img/firefox/new/firefox-logo.png" alt="Firefox">
high_res_img() high_res_img()
^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
For images that include a high-resolution alternative for displays with a high pixel density, use the `high_res_img()` function:: For images that include a high-resolution alternative for displays with a high pixel density, use the `high_res_img()` function:
.. code-block:: python
high_res_img('firefox/new/firefox-logo.png', {'alt': 'Firefox', 'width': '200', 'height': '100'}) high_res_img('firefox/new/firefox-logo.png', {'alt': 'Firefox', 'width': '200', 'height': '100'})
The `high_res_img()` function will automatically look for the image in the URL parameter suffixed with `'-high-res'`, e.g. `firefox/new/firefox-logo-high-res.png` and switch to it if the display has high pixel density. The `high_res_img()` function will automatically look for the image in the URL parameter suffixed with `'-high-res'`, e.g. `firefox/new/firefox-logo-high-res.png` and switch to it if the display has high pixel density.
`high_res_img()` supports localized images by setting the `'l10n'` parameter to `True`:: `high_res_img()` supports localized images by setting the `'l10n'` parameter to `True`:
.. code-block:: python
high_res_img('firefox/new/firefox-logo.png', {'l10n': True, 'alt': 'Firefox', 'width': '200', 'height': '100'}) high_res_img('firefox/new/firefox-logo.png', {'l10n': True, 'alt': 'Firefox', 'width': '200', 'height': '100'})
@ -86,7 +100,9 @@ When using localization, `high_res_img()` will look for images in the appropriat
l10n_img() l10n_img()
^^^^^^^^^^ ^^^^^^^^^^
Images that have translatable text can be handled with `l10n_img()`:: Images that have translatable text can be handled with `l10n_img()`:
.. code-block:: html
<img src="{{ l10n_img('firefox/os/have-it-all/messages.jpg') }}" /> <img src="{{ l10n_img('firefox/os/have-it-all/messages.jpg') }}" />
@ -94,13 +110,17 @@ The images referenced by `l10n_img()` must exist in `media/img/l10n/`, so for ab
platform_img() platform_img()
^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
Finally, for outputting an image that differs depending on the platform being used, the `platform_img()` function will automatically display the image for the user's browser:: Finally, for outputting an image that differs depending on the platform being used, the `platform_img()` function will automatically display the image for the user's browser:
.. code-block:: python
platform_img('firefox/new/browser.png', {'alt': 'Firefox screenshot'}) platform_img('firefox/new/browser.png', {'alt': 'Firefox screenshot'})
`platform_img()` will automatically look for the images `browser-mac.png`, `browser-win.png`, `browser-linux.png`, etc. Platform image also supports hi-res images by adding `'high-res': True` to the list of optional attributes. `platform_img()` will automatically look for the images `browser-mac.png`, `browser-win.png`, `browser-linux.png`, etc. Platform image also supports hi-res images by adding `'high-res': True` to the list of optional attributes.
`platform_img()` supports localized images by setting the `'l10n'` parameter to `True`:: `platform_img()` supports localized images by setting the `'l10n'` parameter to `True`:
.. code-block:: python
platform_img('firefox/new/firefox-logo.png', {'l10n': True, 'alt': 'Firefox screenshot'}) platform_img('firefox/new/firefox-logo.png', {'l10n': True, 'alt': 'Firefox screenshot'})
@ -110,35 +130,57 @@ Writing Views
------------- -------------
You should rarely need to write a view for mozilla.org. Most pages are You should rarely need to write a view for mozilla.org. Most pages are
static and you should use the `page` expression documented above. static and you should use the `page` function documented above.
If you need to write a view and the page has a newsletter signup form If you need to write a view and the page is translated or translatable
in the footer (most do), make sure to handle this in your view. then it should use the `l10n_utils.render()` function to render the
Bedrock comes with a function for doing this automatically:: template.
from bedrock.mozorg.util import handle_newsletter .. code-block:: python
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt from lib import l10n_utils
def view(request):
ctx = handle_newsletter(request) def my_view(request):
# do your fancy things
ctx = {'template_variable': 'awesome data'}
return l10n_utils.render(request, 'app/template.html', ctx) return l10n_utils.render(request, 'app/template.html', ctx)
You'll notice a few other things in there. You should use the
`l10n_utils.render` function to render templates because it handles
special l10n work for us. Since we're handling the newsletter form
post, you also need the `csrf_exempt` decorator.
Make sure to namespace your templates by putting them in a directory Make sure to namespace your templates by putting them in a directory
named after your app, so instead of templates/template.html they would named after your app, so instead of templates/template.html they would
be in templates/blog/template.html if `blog` was the name of your app. be in templates/blog/template.html if `blog` was the name of your app.
If you prefer to use Django's Generic View classes we have a convenient
helper for that. You can use it either to create a custom view class of
your own, or use it directly in a `urls.py` file.
.. code-block:: python
# app/views.py
from lib.l10n_utils import L10nTemplateView
class FirefoxRoxView(L10nTemplateView):
template_name = 'app/firefox-rox.html'
# app/urls.py
urlpatterns = [
# from views.py
path('firefox/rox/', FirefoxRoxView.as_view()),
# directly
path('firefox/sox/', L10nTemplateView.as_view(template_name='app/firefox-sox.html')),
]
The `L10nTemplateView` functionality is mostly in a template mixin called `LangFilesMixin` which
you can use with other generic Django view classes if you need one other than `TemplateView`.
Variation Views Variation Views
^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
We have a generic view that allows you to easily create and use a/b testing We have a generic view that allows you to easily create and use a/b testing
templates. If you'd like to have either separate templates or just a template templates. If you'd like to have either separate templates or just a template
context variable for switching, this will help you out. For example:: context variable for switching, this will help you out. For example.
.. code-block:: python
# urls.py # urls.py
@ -157,12 +199,16 @@ This will give you a context variable called `variation` that will either be an
string if no param is set, or `a` if `?v=a` is in the URL, or `b` if `?v=b` is in the string if no param is set, or `a` if `?v=a` is in the URL, or `b` if `?v=b` is in the
URL. No other options will be valid for the `v` query parameter and `variation` will URL. No other options will be valid for the `v` query parameter and `variation` will
be empty if any other value is passed in for `v` via the URL. So in your template code be empty if any other value is passed in for `v` via the URL. So in your template code
you'd simply do the following:: you'd simply do the following:
.. code-block:: jinja
{% if variation == 'b' %}<p>This is the B variation of our test. Enjoy!</p>{% endif %} {% if variation == 'b' %}<p>This is the B variation of our test. Enjoy!</p>{% endif %}
If you'd rather have a fully separate template for your test, you can use the If you'd rather have a fully separate template for your test, you can use the
`template_name_variations` argument to the view instead of `template_context_variations`:: `template_name_variations` argument to the view instead of `template_context_variations`.
.. code-block:: python
# urls.py # urls.py
@ -191,7 +237,9 @@ You can also limit your variations to certain locales. By default the variations
for any localization of the page, but if you supply a list of locales to the `variation_locales` for any localization of the page, but if you supply a list of locales to the `variation_locales`
argument to the view then it will only set the variation context variable or alter the template argument to the view then it will only set the variation context variable or alter the template
name (depending on the options explained above) when requested at one of said locales. For example, name (depending on the options explained above) when requested at one of said locales. For example,
the template name example above could be modified to only work for English or German like so:: the template name example above could be modified to only work for English or German like so
.. code-block:: python
# urls.py # urls.py
@ -216,6 +264,39 @@ valid variation were given in the URL.
a mixin that implements this pattern that should work with most views: a mixin that implements this pattern that should work with most views:
`bedrock.utils.views.VariationMixin`. `bedrock.utils.views.VariationMixin`.
Geo Redirect View
^^^^^^^^^^^^^^^^^
We sometimes need to have a special page variation for people visiting from certain
countries. To make this easier we have a redirect view class that will allow you to
define URLs per country as well as a default for everyone else. This redirector URL
must only be a redirector since it must be uncachable by our CDN so that all visitors
will hit the server and see the correct page for their location.
.. code-block:: python
from bedrock.base.views import GeoRedirectView
class CanadaIsSpecialView(GeoRedirectView):
geo_urls = {
'CA': 'app.canada-is-special',
}
default_url = 'app.everyone-else'
In this example people in Canada would go to the URL that Django returns using `reverse()`
(i.e. the name of the URL) and everyone else would go to the `app.everyone-else` URL. You
may also use full URLs instead of URL names if you want to. It will look for strings that
start with "http(s)://" and use it as is. The
`country code <https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements>`_
must be 2 characters and upper case. If the patterns for the redirect and the destination(s) have
URL parameters they will be passed to the reverse call for the URL pattern name. So for example
if you're doing this for a Firefox page with a version number in the URL, as long as the view
and destination URLs use the same URL parameter names it will be preserved in the resulting destination URL.
So `/firefox/70.0beta/whatsnew/` would redirect to `/firefox/70.0beta/whatsnew/canada/` for example.
The redirector will also preserve query parameters by default. You can turn that off by
setting the `query_string = False` class variable.
Coding Style Guides Coding Style Guides
------------------- -------------------

Просмотреть файл

@ -10,6 +10,7 @@ from django.http import HttpResponseRedirect
from django.shortcuts import render as django_render from django.shortcuts import render as django_render
from django.template import TemplateDoesNotExist, loader from django.template import TemplateDoesNotExist, loader
from django.utils.translation.trans_real import parse_accept_lang_header from django.utils.translation.trans_real import parse_accept_lang_header
from django.views.generic import TemplateView
from bedrock.base.urlresolvers import split_path from bedrock.base.urlresolvers import split_path
@ -164,7 +165,7 @@ class LangFilesMixin:
add_active_locales = None add_active_locales = None
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super(LangFilesMixin, self).get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
if self.active_locales: if self.active_locales:
ctx['active_locales'] = self.active_locales ctx['active_locales'] = self.active_locales
if self.add_active_locales: if self.add_active_locales:
@ -175,3 +176,7 @@ class LangFilesMixin:
def render_to_response(self, context, **response_kwargs): def render_to_response(self, context, **response_kwargs):
return render(self.request, self.get_template_names(), return render(self.request, self.get_template_names(),
context, **response_kwargs) context, **response_kwargs)
class L10nTemplateView(LangFilesMixin, TemplateView):
pass