зеркало из https://github.com/mozilla/bedrock.git
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:
Родитель
50fa411dfc
Коммит
ed7b0e5a73
|
@ -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):
|
||||
|
|
135
docs/coding.rst
135
docs/coding.rst
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче