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 override_settings
from bedrock.base.views import geolocate
from bedrock.base.views import geolocate, GeoRedirectView
class TestGeolocate(TestCase):
@ -33,3 +33,53 @@ class TestGeolocate(TestCase):
def test_dev_mode(self):
# should match the setting in DEV mode
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 logging
import os.path
import re
from datetime import datetime
from os import getenv
from time import time
@ -9,6 +10,8 @@ import timeago
from django.conf import settings
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
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.csrf import csrf_exempt
from django.views.decorators.http import require_POST, require_safe
@ -31,6 +34,26 @@ def get_geo_from_request(request):
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
@never_cache
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.
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.
Example::
Example:
.. code-block:: python
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
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 data as keyword arguments:
.. code-block:: python
page('channel', 'mozorg/channel.html',
latest_version=product_details.firefox_versions['LATEST_FIREFOX_VERSION'])
@ -62,23 +68,31 @@ Images should be included on pages using helper functions.
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" />
will output an image::
will output an image:
.. code-block:: html
<img src="/media/img/firefox/new/firefox-logo.png" alt="Firefox">
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'})
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'})
@ -86,7 +100,9 @@ When using localization, `high_res_img()` will look for images in the appropriat
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') }}" />
@ -94,13 +110,17 @@ The images referenced by `l10n_img()` must exist in `media/img/l10n/`, so for ab
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()` 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'})
@ -110,35 +130,57 @@ Writing Views
-------------
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
in the footer (most do), make sure to handle this in your view.
Bedrock comes with a function for doing this automatically::
If you need to write a view and the page is translated or translatable
then it should use the `l10n_utils.render()` function to render the
template.
from bedrock.mozorg.util import handle_newsletter
from django.views.decorators.csrf import csrf_exempt
.. code-block:: python
@csrf_exempt
def view(request):
ctx = handle_newsletter(request)
from lib import l10n_utils
def my_view(request):
# do your fancy things
ctx = {'template_variable': 'awesome data'}
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
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.
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
^^^^^^^^^^^^^^^
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
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
@ -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
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
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 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
@ -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`
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,
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
@ -216,6 +264,39 @@ valid variation were given in the URL.
a mixin that implements this pattern that should work with most views:
`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
-------------------

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

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