diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..02ea720 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,74 @@ +name: test + +on: [pull_request, push] + +jobs: + + lint: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: 3.8 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox + + - name: Run tox -e lint + run: tox + env: + TOXENV: lint + + test: + name: unittests + runs-on: ubuntu-latest + + strategy: + matrix: + toxenv: + - py36-dj22-wag27 + - py36-dj22-waglatest + - py36-dj31-waglatest + - py38-dj22-wag27 + - py38-dj22-waglatest + - py38-dj31-waglatest + include: + - toxenv: py36-dj22-wag27 + python-version: 3.6 + - toxenv: py36-dj22-waglatest + python-version: 3.6 + - toxenv: py36-dj31-waglatest + python-version: 3.6 + - toxenv: py38-dj22-wag27 + python-version: 3.8 + - toxenv: py38-dj22-waglatest + python-version: 3.8 + - toxenv: py38-dj31-waglatest + python-version: 3.8 + + steps: + - uses: actions/checkout@v1 + + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox coveralls + + - name: Run tox + run: | + tox + coveralls + env: + TOXENV: ${{ matrix.toxenv }} + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9698f07..0000000 --- a/.travis.yml +++ /dev/null @@ -1,28 +0,0 @@ -language: python -cache: pip - -matrix: - include: - - env: TOXENV=lint - python: 3.6 - - env: TOXENV=py36-dj111-wag23 - python: 3.6 - - env: TOXENV=py36-dj22-wag23 - python: 3.6 - - env: TOXENV=py36-dj22-wag29 - python: 3.6 - - env: TOXENV=py38-dj22-wag29 - python: 3.8 - - env: TOXENV=py36-dj22-wag210 - python: 3.6 - - env: TOXENV=py38-dj22-wag210 - python: 3.8 - -install: - pip install tox coveralls - -script: - tox - -after_success: - coveralls diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 590afb1..d6bd468 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,11 +5,10 @@ > feature request, you are agreeing to comply with this waiver of copyright interest. > Details can be found in our [TERMS](TERMS.md) and [LICENCE](LICENSE). - There are two primary ways to help: - - Using the issue tracker, and - - Changing the code-base. +- Using the issue tracker, and +- Changing the code-base. ## Using the issue tracker @@ -21,7 +20,6 @@ Use the issue tracker to find ways to contribute. Find a bug or a feature, menti the issue that you will take on that effort, then follow the _Changing the code-base_ guidance below. - ## Changing the code-base Generally speaking, you should fork this repository, make changes in your @@ -31,7 +29,6 @@ Additionally, the code should follow any stylistic and architectural guidelines prescribed by the project. In the absence of such guidelines, mimic the styles and patterns in the existing code-base. - ## Style This project uses [`black`](https://github.com/psf/black) to format code, @@ -42,7 +39,7 @@ You can format code and imports by calling: ``` black wagtailsharing -isort --recursive wagtailsharing +isort wagtailsharing ``` And you can check for style, import order, and other linting by using: diff --git a/README.rst b/README.rst index a82f5d3..b5f031f 100644 --- a/README.rst +++ b/README.rst @@ -1,9 +1,9 @@ -.. image:: https://travis-ci.org/cfpb/wagtail-sharing.svg?branch=master +.. image:: https://github.com/cfpb/wagtail-sharing/workflows/test/badge.svg?branch=main :alt: Build Status - :target: https://travis-ci.org/cfpb/wagtail-sharing -.. image:: https://coveralls.io/repos/github/cfpb/wagtail-sharing/badge.svg?branch=master + :target: https://github.com/cfpb/wagtail-sharing/actions?query=branch%3Amain+workflow%3Atest+ +.. image:: https://coveralls.io/repos/github/cfpb/wagtail-sharing/badge.svg?branch=main :alt: Coverage Status - :target: https://coveralls.io/github/cfpb/wagtail-sharing?branch=master + :target: https://coveralls.io/github/cfpb/wagtail-sharing?branch=main wagtail-sharing =============== @@ -30,7 +30,7 @@ Install the package using pip: .. code-block:: bash $ pip install wagtail-sharing - + Add ``wagtailsharing`` as an installed app in your Django settings: .. code-block:: python @@ -49,7 +49,7 @@ Run migrations to create required database tables: .. code-block:: bash $ manage.py migrate wagtailsharing - + Replace use of Wagtail's catch-all URL pattern: .. code-block:: diff @@ -66,9 +66,9 @@ Replace use of Wagtail's catch-all URL pattern: Sharing sites ------------- -The Wagtail admin now contains a new section under Settings called Sharing Sites that allows users to define how they would like to expose latest page revisions. +The Wagtail admin now contains a new section under Settings called Sharing Sites that allows users to define how they would like to expose latest page revisions. -.. image:: https://raw.githubusercontent.com/cfpb/wagtail-sharing/master/docs/images/sharing-sites.png +.. image:: https://raw.githubusercontent.com/cfpb/wagtail-sharing/main/docs/images/sharing-sites.png :width: 200px :height: 100px :alt: Sharing sites @@ -90,7 +90,7 @@ Verify that you can access your local server at http://sharing.localhost:8000. Y To do so, in the Wagtail admin, under Settings, Sharing Sites, create a new sharing site for the default site, with hostname ``sharing.localhost`` and port ``8000``. -.. image:: https://raw.githubusercontent.com/cfpb/wagtail-sharing/master/docs/images/new-sharing-site.png +.. image:: https://raw.githubusercontent.com/cfpb/wagtail-sharing/main/docs/images/new-sharing-site.png :width: 200px :height: 100px :alt: New sharing site with site: "localhost [default]", hostname: "sharing.localhost", port: "8000" @@ -102,7 +102,7 @@ Banners Pages viewed on a wagtail-sharing shared site have a simple banner added to them to remind reviewers that the current published content may differ from the content they are viewing. -.. image:: https://raw.githubusercontent.com/cfpb/wagtail-sharing/master/docs/images/banner.png +.. image:: https://raw.githubusercontent.com/cfpb/wagtail-sharing/main/docs/images/banner.png :alt: Banner This behavior can be disabled by setting ``settings.WAGTAILSHARING_BANNER = False``. The banner template can be overridden by providing an alternate template file at ``wagtailsharing/banner.html`` similar to how `wagtailadmin template overrides `_ are supported. @@ -114,9 +114,21 @@ A page's sharing URL can be retrieved by passing its ``Page`` instance to ``wagt Shared pages will also have a new dropdown menu option that links to this sharing URL from the Wagtail page explorer. -.. image:: https://raw.githubusercontent.com/cfpb/wagtail-sharing/master/docs/images/dropdown.png +.. image:: https://raw.githubusercontent.com/cfpb/wagtail-sharing/main/docs/images/dropdown.png :alt: Dropdown with sharing link +To use tokens in place of the page path on the sharing site, add the following setting: + +.. code-block:: python + + WAGTAILSHARING_TOKENIZE_URL = True + +In some environments such as Heroku, SERVER_PORT set at release time and the real port is fowarded, so the following is needed in settings: + +.. code-block:: python + + USE_X_FORWARDED_PORT = True + Hooks ----- @@ -162,9 +174,9 @@ Compatibility This project has been tested for compatibility with: -* Python 3.6, 3.8 -* Django 1.11, 2.2 -* Wagtail 2.3, 2.9, 2.10 +* Python 3.6+ +* Django 2.2 (LTS), 3.1 (current) +* Wagtail 2.7 (LTS), 2.10 (current) It should be compatible with all intermediate versions, as well. If you find that it is not, please `file an issue `_. @@ -172,6 +184,6 @@ If you find that it is not, please `file an issue `_ -#. `LICENSE `_ +#. `TERMS `_ +#. `LICENSE `_ #. `CFPB Source Code Policy `_ diff --git a/setup.py b/setup.py index f8846d8..6cd4141 100644 --- a/setup.py +++ b/setup.py @@ -2,16 +2,11 @@ install_requires = [ - "wagtail>=2.3,<2.11", -] - - -testing_extras = [ - "coverage>=3.7.0", - "flake8>=2.2.0", - "mock>=1.0.0", + "wagtail>=2.7,<3", + "pyjwt>1.7,<2.0", ] +testing_extras = ["coverage>=3.7.0"] setup( name="wagtail-sharing", @@ -19,18 +14,18 @@ author="CFPB", author_email="tech@cfpb.gov", license="CCO", - version="2.2.1", + version="2.3.0", include_package_data=True, packages=find_packages(), python_requires=">=3.6", install_requires=install_requires, extras_require={"testing": testing_extras}, description="Easier sharing of Wagtail drafts", - long_description=open("README.rst").read(), + long_description=open("README.rst", "r", encoding="utf-8").read(), classifiers=[ "Framework :: Django", - "Framework :: Django :: 1.11", "Framework :: Django :: 2.2", + "Framework :: Django :: 3.1", "Framework :: Wagtail", "Framework :: Wagtail :: 2", "License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication", diff --git a/tox.ini b/tox.ini index cf65862..dcef9ff 100644 --- a/tox.ini +++ b/tox.ini @@ -2,13 +2,14 @@ skipsdist=True envlist= lint, - py{36}-dj{111,22}-wag{23}, - py{36,38}-dj{22}-wag{29,210} + py{36,38}-dj{22,31}-wag{27,latest} [testenv] install_command=pip install -e ".[testing]" -U {opts} {packages} commands= + coverage erase coverage run {envbindir}/django-admin.py test {posargs} + coverage report -m setenv= DJANGO_SETTINGS_MODULE=wagtailsharing.tests.settings @@ -18,14 +19,12 @@ basepython= deps= mock>=1.0.0 - dj111: Django>=1.11,<1.12 dj22: Django>=2.2,<2.3 - wag23: wagtail>=2.3,<2.4 - wag29: wagtail>=2.9,<2.10 - wag210: wagtail>=2.10,<2.11 + dj31: Django>=3.1,<3.2 + wag27: wagtail>=2.7,<2.8 + waglatest: wagtail<3 [testenv:lint] -recreate=False basepython=python3.6 deps= black @@ -33,7 +32,7 @@ deps= isort commands= black --check wagtailsharing setup.py - flake8 wagtailsharing + flake8 wagtailsharing setup.py isort --check-only --diff wagtailsharing [flake8] @@ -53,6 +52,6 @@ multi_line_output=3 skip=.tox,migrations use_parentheses=1 known_django=django -known_future_library=future +known_wagtail=wagtail default_section=THIRDPARTY -sections=FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER +sections=FUTURE,STDLIB,DJANGO,WAGTAIL,THIRDPARTY,FIRSTPARTY,LOCALFOLDER diff --git a/wagtailsharing/helpers.py b/wagtailsharing/helpers.py index 582db55..9aa4448 100644 --- a/wagtailsharing/helpers.py +++ b/wagtailsharing/helpers.py @@ -1,8 +1,25 @@ +from django.conf import settings + +import jwt from wagtail.core.models import Site from wagtailsharing.models import SharingSite +def get_tokenized_sharing_url(sharing_site, page_path): + share_path = getattr(settings, "WAGTAILSHARING_TOKEN_SHARE_PATH", "share") + payload = {"path": page_path} + return "/".join( + [ + sharing_site.root_url, + share_path, + jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256").decode( + "utf-8" + ), + ] + ) + + def get_sharing_url(page): """Get a sharing URL for the latest revision of a page, if available.""" url_parts = page.get_url_parts() @@ -21,4 +38,7 @@ def get_sharing_url(page): # Site is not shared. return None + if getattr(settings, "WAGTAILSHARING_TOKENIZE_URL", False): + return get_tokenized_sharing_url(sharing_site, page_path) + return sharing_site.root_url + page_path diff --git a/wagtailsharing/tests/test_urls.py b/wagtailsharing/tests/test_urls.py index ca1a75c..ab94518 100644 --- a/wagtailsharing/tests/test_urls.py +++ b/wagtailsharing/tests/test_urls.py @@ -1,19 +1,14 @@ from importlib import reload +from unittest.mock import patch from django.test import TestCase +from django.urls import re_path -from mock import patch from wagtail.core import urls as wagtail_core_urls import wagtailsharing.urls -try: - from django.urls import re_path -except ImportError: - from django.conf.urls import url as re_path - - class TestUrlPatterns(TestCase): def setUp(self): def test_view(): diff --git a/wagtailsharing/tests/test_views.py b/wagtailsharing/tests/test_views.py index 58daba2..68a07bb 100644 --- a/wagtailsharing/tests/test_views.py +++ b/wagtailsharing/tests/test_views.py @@ -1,8 +1,9 @@ +from unittest.mock import patch + from django.http import Http404, HttpResponse from django.test import RequestFactory, TestCase import wagtail -from mock import patch from wagtail.core.models import Site from wagtail.tests.utils import WagtailTestUtils diff --git a/wagtailsharing/tests/test_wagtail_hooks.py b/wagtailsharing/tests/test_wagtail_hooks.py index 8531042..819d7b1 100644 --- a/wagtailsharing/tests/test_wagtail_hooks.py +++ b/wagtailsharing/tests/test_wagtail_hooks.py @@ -2,9 +2,10 @@ from django.http import HttpResponse from django.test import TestCase, override_settings -from mock import Mock, patch from wagtail.tests.testapp.models import SimplePage +from mock import Mock, patch + from wagtailsharing.wagtail_hooks import add_sharing_banner, add_sharing_link diff --git a/wagtailsharing/urls.py b/wagtailsharing/urls.py index 07bddf6..833f47f 100644 --- a/wagtailsharing/urls.py +++ b/wagtailsharing/urls.py @@ -1,9 +1,12 @@ +from django.conf import settings +from django.conf.urls import url + from wagtail.core.urls import ( serve_pattern, urlpatterns as wagtailcore_urlpatterns, ) -from wagtailsharing.views import ServeView +from wagtailsharing.views import ServeView, TokenServeView try: @@ -12,9 +15,19 @@ from django.conf.urls import url as re_path -urlpatterns = [ - re_path(serve_pattern, ServeView.as_view(), name="wagtail_serve") - if urlpattern.name == "wagtail_serve" - else urlpattern - for urlpattern in wagtailcore_urlpatterns -] +if getattr(settings, "WAGTAILSHARING_TOKENIZE_URL", False): + share_path = getattr(settings, "WAGTAILSHARING_TOKEN_SHARE_PATH", "share") + urlpatterns = wagtailcore_urlpatterns + [ + url( + rf"^{share_path}/([\w\.\-\_]+)/$", + TokenServeView.as_view(), + name="wagtail_serve", + ), + ] +else: + urlpatterns = [ + re_path(serve_pattern, ServeView.as_view(), name="wagtail_serve") + if urlpattern.name == "wagtail_serve" + else urlpattern + for urlpattern in wagtailcore_urlpatterns + ] diff --git a/wagtailsharing/views.py b/wagtailsharing/views.py index 1d5a4be..cda6ab4 100644 --- a/wagtailsharing/views.py +++ b/wagtailsharing/views.py @@ -1,8 +1,11 @@ import inspect +import logging +from django.conf import settings from django.http import Http404, HttpResponse from django.views.generic import View +import jwt from wagtail.contrib.routable_page.models import RoutablePageMixin from wagtail.core import hooks from wagtail.core.url_routing import RouteResult @@ -12,15 +15,20 @@ class ServeView(View): - def dispatch(self, request, path): - if request.method.upper() != "GET": - return wagtail_serve(request, path) - + def get_sharing_site(self, request, path): try: sharing_site = SharingSite.find_for_request(request) except SharingSite.DoesNotExist: sharing_site = None + return sharing_site + + def dispatch(self, request, path): + if request.method.upper() != "GET": + return wagtail_serve(request, path) + + sharing_site = self.get_sharing_site(request, path) + if not sharing_site: return wagtail_serve(request, path) @@ -99,3 +107,30 @@ def serve(page, request, args, kwargs): return result return response + + +class TokenServeView(ServeView): + def dispatch(self, request, path): + if request.method.upper() != "GET": + return wagtail_serve(request, path) + + sharing_site = self.get_sharing_site(request, path) + + if not sharing_site: + return wagtail_serve(request, path) + + try: + # Get the wagtail path from the JWT token + data = jwt.decode(path, settings.SECRET_KEY, algorithms=["HS256"]) + decoded_path = data["path"] + except Exception as e: + logging.warn( + f"Could not decode Wagtail path from sharing link: {e}" + ) + return wagtail_serve(request, path) + + page, args, kwargs = self.route( + sharing_site.site, request, decoded_path + ) + + return self.serve(page, request, args, kwargs)