Merge branch 'main' into dependabot/pip/psycopg2-binary-2.9.6
This commit is contained in:
Коммит
43e95a63c7
|
@ -0,0 +1,10 @@
|
|||
[coverage:run]
|
||||
source = netwrok-api/networkapi
|
||||
omit =
|
||||
*migrations*
|
||||
*settings.py*
|
||||
*test_*
|
||||
branch = true
|
||||
|
||||
[coverage:report]
|
||||
show_missing = True
|
|
@ -86,6 +86,8 @@ jobs:
|
|||
run: |
|
||||
npm run build
|
||||
python network-api/manage.py collectstatic --no-input --verbosity 0
|
||||
python network-api/manage.py check
|
||||
python network-api/manage.py makemigrations --check --dry-run
|
||||
python network-api/manage.py migrate --no-input
|
||||
python network-api/manage.py block_inventory
|
||||
python network-api/manage.py compilemessages
|
||||
|
@ -105,7 +107,7 @@ jobs:
|
|||
- name: Run type checks
|
||||
run: mypy network-api
|
||||
- name: Run Tests
|
||||
run: coverage run --source './network-api/networkapi' network-api/manage.py test networkapi
|
||||
run: cd network-api && pytest --ds=networkapi.settings -cov=network-api/networkapi --cov-report=term-missing
|
||||
- name: Coveralls
|
||||
run: coveralls
|
||||
continue-on-error: true
|
||||
|
@ -143,9 +145,10 @@ jobs:
|
|||
USE_S3: False
|
||||
X_FRAME_OPTIONS: DENY
|
||||
XSS_PROTECTION: True
|
||||
CSP_FONT_SRC: "'self' https://fonts.gstatic.com https://fonts.googleapis.com https://code.cdn.mozilla.net"
|
||||
CSP_SCRIPT_SRC: "'self' 'unsafe-inline' https://www.google-analytics.com/analytics.js http://*.shpg.org/ https://comments.mozillafoundation.org/ https://airtable.com https://platform.twitter.com https://cdnjs.cloudflare.com/ajax/libs/gsap/3.8.0/gsap.min.js https://cdnjs.cloudflare.com/ajax/libs/gsap/3.8.0/ScrollTrigger.min.js"
|
||||
CSP_STYLE_SRC: "'self' 'unsafe-inline' https://code.cdn.mozilla.net https://fonts.googleapis.com https://platform.twitter.com"
|
||||
CSP_CONNECT_SRC: "*"
|
||||
CSP_FONT_SRC: "'self' https://fonts.gstatic.com https://fonts.googleapis.com https://code.cdn.mozilla.net https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/fonts/ data:"
|
||||
CSP_SCRIPT_SRC: "'self' 'unsafe-inline' https://www.google-analytics.com/analytics.js http://*.shpg.org/ https://comments.mozillafoundation.org/ https://airtable.com https://platform.twitter.com https://cdnjs.cloudflare.com/ajax/libs/gsap/3.8.0/gsap.min.js https://cdnjs.cloudflare.com/ajax/libs/gsap/3.8.0/ScrollTrigger.min.js https://*.googletagmanager.com https://*.fundraiseup.com https://mozillafoundation.tfaforms.net 'unsafe-eval'"
|
||||
CSP_STYLE_SRC: "'self' 'unsafe-inline' https://code.cdn.mozilla.net https://fonts.googleapis.com https://platform.twitter.com https://mozillafoundation.tfaforms.net https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
|
|
20
README.md
20
README.md
|
@ -77,25 +77,41 @@ When relevant, we encourage you to write tests.
|
|||
|
||||
You can run the tests using `inv test`.
|
||||
This will the full test suite.
|
||||
|
||||
To run only a subset or a specific Python test, you can use following command:
|
||||
|
||||
```console
|
||||
inv manage "test <dotted-path-to-your-test>"
|
||||
inv test-python --file path/to/file.py
|
||||
```
|
||||
|
||||
The `test-python` command also support flags for turning increased verbosity on/off (`-v`) and
|
||||
for running tests in parallel (the `-n` option). To run tests with 4 parallel processes and increased
|
||||
verbosity, use:
|
||||
|
||||
```console
|
||||
inv test-python -v -n 4
|
||||
```
|
||||
|
||||
The `-n` flag also supports the `auto` value, which will run tests with as many parallel cores as possible.
|
||||
For more info, consult the [pytest-xdist docs](https://pytest-xdist.readthedocs.io/en/stable/distribution.html).
|
||||
|
||||
See also [the Django docs on running tests](https://docs.djangoproject.com/en/4.1/topics/testing/overview/#running-tests).
|
||||
|
||||
There is currently no unit test framework for JavaScript tests set up.
|
||||
|
||||
### Integration tests
|
||||
|
||||
**(Note that this is still a work in progress.)**
|
||||
|
||||
Integration testing is done using [Playwright](https://playwright.dev/), with the integration tests found in `./tests/integration`.
|
||||
|
||||
You can run these tests locally by running a one-time `npm install` and `npm run playwright:install` after which you should be able to run `npm run playwright` to run the visual tests, with `docker-compose up` running in a secondary terminal.
|
||||
|
||||
In order to run the same tests as will run during CI testing, make sure that `RANDOM_SEED=530910203` is set in your `.env` file, and that your local database is a new db based on that seed (`inv new-db`).
|
||||
|
||||
Note that this is still a work in progress.
|
||||
#### URL checker
|
||||
|
||||
URL checker can be initiated by running `docker-compose up` in one terminal and running `npm run playwright:urls` in a secondary terminal. It checks to see if visiting the URLs listed in [`tests/foundation-urls.js`](https://github.com/MozillaFoundation/foundation.mozilla.org/blob/main/tests/foundation-urls.js) and [`tests/mozfest-urls.js`](https://github.com/MozillaFoundation/foundation.mozilla.org/blob/main/tests/mozfest-urls.js) returns an OK response (i.e., status 200). Note that the URL lists in these two files are not complete and will require updates. We will also need to expand the lists to include PNI and Donate URLs.
|
||||
|
||||
### Visual regression tests
|
||||
|
||||
|
|
6
app.json
6
app.json
|
@ -28,11 +28,11 @@
|
|||
"CSP_DEFAULT_SRC": "'none'",
|
||||
"CSP_FRAME_ANCESTORS": "'none'",
|
||||
"CSP_FRAME_SRC": "'self' https://js.tito.io",
|
||||
"CSP_FONT_SRC": "'self' https://fonts.gstatic.com https://fonts.googleapis.com https://code.cdn.mozilla.net",
|
||||
"CSP_FONT_SRC": "'self' https://fonts.gstatic.com https://fonts.googleapis.com https://code.cdn.mozilla.net https://static.fundraiseup.com/fonts/ https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/fonts/",
|
||||
"CSP_IMG_SRC": "* data:",
|
||||
"CSP_MEDIA_SRC": "'self' https://s3.amazonaws.com/mofo-assets/foundation/video/",
|
||||
"CSP_SCRIPT_SRC": "'self' 'unsafe-inline' https://www.google-analytics.com/analytics.js http://*.shpg.org/ https://comments.mozillafoundation.org/ https://airtable.com https://platform.twitter.com https://cdn.syndication.twimg.com https://js.tito.io https://js-plugins.tito.io/gtm.js",
|
||||
"CSP_STYLE_SRC": "'self' 'unsafe-inline' https://code.cdn.mozilla.net https://fonts.googleapis.com https://platform.twitter.com https://js.tito.io",
|
||||
"CSP_SCRIPT_SRC": "'self' 'unsafe-inline' https://www.google-analytics.com/analytics.js http://*.shpg.org/ https://comments.mozillafoundation.org/ https://airtable.com https://platform.twitter.com https://cdn.syndication.twimg.com https://js.tito.io https://js-plugins.tito.io/gtm.js https://*.fundraiseup.com *.googletagmanager.com https://mozillafoundation.tfaforms.net 'unsafe-eval'",
|
||||
"CSP_STYLE_SRC": "'self' 'unsafe-inline' https://code.cdn.mozilla.net https://fonts.googleapis.com https://platform.twitter.com https://js.tito.io https://mozillafoundation.tfaforms.net https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css",
|
||||
"NPM_CONFIG_PRODUCTION": "true",
|
||||
"REVIEW_APP": "True",
|
||||
"XROBOTSTAG_ENABLED": "True"
|
||||
|
|
|
@ -10,3 +10,9 @@ mypy
|
|||
ptvsd
|
||||
types-python-slugify
|
||||
types-requests
|
||||
|
||||
pytest
|
||||
pytest-django
|
||||
pytest-cov
|
||||
pytest-xdist
|
||||
pytest-sugar
|
|
@ -8,9 +8,9 @@ asgiref==3.5.0
|
|||
# via
|
||||
# -c requirements.txt
|
||||
# django
|
||||
black==22.10.0
|
||||
black==22.12.0
|
||||
# via -r dev-requirements.in
|
||||
certifi==2021.10.8
|
||||
certifi==2022.12.7
|
||||
# via
|
||||
# -c requirements.txt
|
||||
# requests
|
||||
|
@ -29,18 +29,20 @@ click==8.1.3
|
|||
# djlint
|
||||
colorama==0.4.6
|
||||
# via djlint
|
||||
coverage==5.5
|
||||
# via coveralls
|
||||
coverage[toml]==5.5
|
||||
# via
|
||||
# coveralls
|
||||
# pytest-cov
|
||||
coveralls==3.3.1
|
||||
# via -r dev-requirements.in
|
||||
cryptography==36.0.1
|
||||
cryptography==39.0.1
|
||||
# via
|
||||
# -c requirements.txt
|
||||
# pyopenssl
|
||||
# urllib3
|
||||
cssbeautifier==1.14.7
|
||||
# via djlint
|
||||
django==3.2.16
|
||||
django==3.2.19
|
||||
# via
|
||||
# -c requirements.txt
|
||||
# django-debug-toolbar
|
||||
|
@ -56,6 +58,10 @@ editorconfig==0.12.3
|
|||
# via
|
||||
# cssbeautifier
|
||||
# jsbeautifier
|
||||
exceptiongroup==1.1.1
|
||||
# via pytest
|
||||
execnet==1.9.0
|
||||
# via pytest-xdist
|
||||
flake8==3.9.2
|
||||
# via -r dev-requirements.in
|
||||
html-tag-names==0.1.2
|
||||
|
@ -67,7 +73,9 @@ idna==3.3
|
|||
# -c requirements.txt
|
||||
# requests
|
||||
# urllib3
|
||||
isort==5.10.1
|
||||
iniconfig==2.0.0
|
||||
# via pytest
|
||||
isort==5.12.0
|
||||
# via -r dev-requirements.in
|
||||
jsbeautifier==1.14.7
|
||||
# via
|
||||
|
@ -81,12 +89,18 @@ mypy-extensions==0.4.3
|
|||
# via
|
||||
# black
|
||||
# mypy
|
||||
packaging==21.3
|
||||
# via
|
||||
# pytest
|
||||
# pytest-sugar
|
||||
pathspec==0.11.1
|
||||
# via
|
||||
# black
|
||||
# djlint
|
||||
platformdirs==2.5.3
|
||||
# via black
|
||||
pluggy==1.0.0
|
||||
# via pytest
|
||||
ptvsd==4.3.2
|
||||
# via -r dev-requirements.in
|
||||
pycodestyle==2.7.0
|
||||
|
@ -97,10 +111,27 @@ pycparser==2.21
|
|||
# cffi
|
||||
pyflakes==2.3.1
|
||||
# via flake8
|
||||
pyopenssl==22.0.0
|
||||
pyopenssl==23.0.0
|
||||
# via
|
||||
# -c requirements.txt
|
||||
# urllib3
|
||||
pyparsing==3.0.7
|
||||
# via packaging
|
||||
pytest==7.3.1
|
||||
# via
|
||||
# -r dev-requirements.in
|
||||
# pytest-cov
|
||||
# pytest-django
|
||||
# pytest-sugar
|
||||
# pytest-xdist
|
||||
pytest-cov==4.0.0
|
||||
# via -r dev-requirements.in
|
||||
pytest-django==4.5.2
|
||||
# via -r dev-requirements.in
|
||||
pytest-sugar==0.9.7
|
||||
# via -r dev-requirements.in
|
||||
pytest-xdist==3.2.1
|
||||
# via -r dev-requirements.in
|
||||
pytz==2021.3
|
||||
# via
|
||||
# -c requirements.txt
|
||||
|
@ -118,16 +149,23 @@ six==1.16.0
|
|||
# -c requirements.txt
|
||||
# cssbeautifier
|
||||
# jsbeautifier
|
||||
sqlparse==0.4.2
|
||||
sqlparse==0.4.4
|
||||
# via
|
||||
# -c requirements.txt
|
||||
# django
|
||||
# django-debug-toolbar
|
||||
termcolor==2.3.0
|
||||
# via pytest-sugar
|
||||
toml==0.10.2
|
||||
# via
|
||||
# -c requirements.txt
|
||||
# coverage
|
||||
tomli==2.0.1
|
||||
# via
|
||||
# black
|
||||
# djlint
|
||||
# mypy
|
||||
# pytest
|
||||
tqdm==4.63.0
|
||||
# via
|
||||
# -c requirements.txt
|
||||
|
@ -143,7 +181,11 @@ typing-extensions==4.4.0
|
|||
# -c requirements.txt
|
||||
# black
|
||||
# mypy
|
||||
urllib3[secure]==1.26.8
|
||||
urllib3[secure]==1.26.15
|
||||
# via
|
||||
# -c requirements.txt
|
||||
# requests
|
||||
urllib3-secure-extra==0.1.0
|
||||
# via
|
||||
# -c requirements.txt
|
||||
# urllib3
|
||||
|
|
|
@ -57,13 +57,13 @@ CRM_PETITION_SQS_QUEUE_URL=
|
|||
CSP_CHILD_SRC=" 'self' https://www.youtube.com https://www.youtube-nocookie.com "
|
||||
CSP_CONNECT_SRC=" * "
|
||||
CSP_DEFAULT_SRC=" 'none' "
|
||||
CSP_FONT_SRC=" 'self' data: https://fonts.gstatic.com https://fonts.googleapis.com https://code.cdn.mozilla.net "
|
||||
CSP_FONT_SRC=" 'self' data: https://fonts.gstatic.com https://fonts.googleapis.com https://code.cdn.mozilla.net https://static.fundraiseup.com/fonts/ https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/fonts/"
|
||||
CSP_FRAME_ANCESTORS=" 'none' "
|
||||
CSP_FRAME_SRC=" 'self' https://www.youtube.com https://comments.mozillafoundation.org/ https://airtable.com https://docs.google.com/ https://platform.twitter.com https://public.zenkit.com https://calendar.google.com https://www.youtube-nocookie.com https://form.typeform.com https://js.tito.io https://datawrapper.dwcdn.net"
|
||||
CSP_IMG_SRC=" * data: "
|
||||
CSP_MEDIA_SRC=" 'self' data: https://s3.amazonaws.com/mofo-assets/foundation/video/ "
|
||||
CSP_SCRIPT_SRC=" 'self' 'unsafe-inline' https://www.google-analytics.com/analytics.js http://*.shpg.org/ https://comments.mozillafoundation.org/ https://airtable.com https://platform.twitter.com https://cdn.syndication.twimg.com https://embed.typeform.com https://js.tito.io https://js-plugins.tito.io/gtm.js https://tagmanager.google.com *.googletagmanager.com"
|
||||
CSP_STYLE_SRC=" 'self' 'unsafe-inline' https://code.cdn.mozilla.net https://fonts.googleapis.com https://platform.twitter.com https://js.tito.io https://tagmanager.google.com"
|
||||
CSP_SCRIPT_SRC=" 'self' 'unsafe-inline' https://www.google-analytics.com/analytics.js http://*.shpg.org/ https://comments.mozillafoundation.org/ https://airtable.com https://platform.twitter.com https://cdn.syndication.twimg.com https://embed.typeform.com https://js.tito.io https://js-plugins.tito.io/gtm.js https://tagmanager.google.com *.googletagmanager.com https://*.fundraiseup.com https://mozillafoundation.tfaforms.net 'unsafe-eval'"
|
||||
CSP_STYLE_SRC=" 'self' 'unsafe-inline' https://code.cdn.mozilla.net https://fonts.googleapis.com https://platform.twitter.com https://js.tito.io https://tagmanager.google.com https://mozillafoundation.tfaforms.net https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
|
||||
CSP_INCLUDE_NONCE_IN=script-src
|
||||
|
||||
|
||||
|
|
|
@ -152,7 +152,13 @@ def signup_submission(request, signup):
|
|||
data["campaign_id"] = cid
|
||||
|
||||
# Subscribing to newsletter using basket.
|
||||
response = basket.subscribe(data["email"], data["newsletters"], lang=data["lang"])
|
||||
# https://basket-client.readthedocs.io/en/latest/usage.html
|
||||
basket_additional = {"lang": data["lang"], "source_url": data["source_url"]}
|
||||
|
||||
if data["country"] != "":
|
||||
basket_additional["country"] = data["country"]
|
||||
|
||||
response = basket.subscribe(data["email"], data["newsletters"], **basket_additional)
|
||||
if response["status"] == "ok":
|
||||
return JsonResponse(data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
@ -222,7 +228,13 @@ def petition_submission(request, petition):
|
|||
|
||||
# Use basket-clients subscribe method, then send the petition information to SQS
|
||||
# with "newsletterSignup" set to false, to avoid subscribing them twice.
|
||||
basket.subscribe(data["email"], "mozilla-foundation", lang=data["lang"])
|
||||
# https://basket-client.readthedocs.io/en/latest/usage.html
|
||||
basket_additional = {"lang": data["lang"], "source_url": data["source_url"]}
|
||||
|
||||
if "country" in data:
|
||||
basket_additional["country"] = data["country"]
|
||||
|
||||
basket.subscribe(data["email"], "mozilla-foundation", **basket_additional)
|
||||
data["newsletterSignup"] = False
|
||||
|
||||
return send_to_sqs(crm_sqs["client"], crm_queue_url, message, type="petition")
|
||||
|
|
|
@ -13,7 +13,7 @@ class BaseDonationPage(FoundationMetadataPageMixin, Page):
|
|||
|
||||
|
||||
class DonateLandingPage(BaseDonationPage):
|
||||
template = "pages/landing_page.html"
|
||||
template = "donate/pages/landing_page.html"
|
||||
|
||||
# Only allow creating landing pages at the root level
|
||||
parent_page_types = ["wagtailcore.Page"]
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<h1> {{ page.title }} </h1>
|
|
@ -61,5 +61,5 @@ class DonateLandingPageTest(test_base.WagtailpagesTestCase):
|
|||
|
||||
self.assertTemplateUsed(
|
||||
response=response,
|
||||
template_name="pages/landing_page.html",
|
||||
template_name="donate/pages/landing_page.html",
|
||||
)
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
from django.db.utils import IntegrityError
|
||||
from wagtail.contrib.redirects.models import Redirect
|
||||
from wagtail.models import Locale
|
||||
|
||||
# Define the list of valid values to check against
|
||||
LOCALES = Locale.objects.all().values_list("language_code", flat=True)
|
||||
|
||||
|
||||
def is_starting_with_language_code(url_path):
|
||||
"""
|
||||
This function takes a URL path as input and checks if the first part of the path is a valid language code.
|
||||
|
||||
Args:
|
||||
url_path (str): A string representing the URL path to be checked.
|
||||
|
||||
Returns:
|
||||
bool: True if the first part of the URL path is a valid language code, otherwise False.
|
||||
|
||||
Example:
|
||||
>>> is_starting_with_language_code("/en/example")
|
||||
True
|
||||
>>> is_starting_with_language_code("/fr/example")
|
||||
True
|
||||
>>> is_starting_with_language_code("/example")
|
||||
False
|
||||
"""
|
||||
|
||||
# Extract the first part of the URL path
|
||||
path_parts = url_path.split("/")
|
||||
first_part = path_parts[1] if len(path_parts) > 1 else ""
|
||||
|
||||
# Check if the first part of the URL path is in the valid list of values
|
||||
if first_part in LOCALES:
|
||||
# do nothing
|
||||
return True
|
||||
else:
|
||||
# Create a redirect for this path
|
||||
return False
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Creates redirects for each locale for each redirect if it doesn't exist."
|
||||
|
||||
def handle(self, *args, **options):
|
||||
redirects = Redirect.objects.all()
|
||||
|
||||
for redirect in redirects:
|
||||
has_locale_redirect = is_starting_with_language_code(url_path=redirect.old_path)
|
||||
if not has_locale_redirect:
|
||||
# create a new redirect for each locale, it will be a copy
|
||||
# of the original redirect but with the locale prefix
|
||||
for locale in LOCALES:
|
||||
# first check if the redirect already exists, this will happen
|
||||
# if we run this code more than once
|
||||
if not redirects.filter(old_path=f"/{locale}{redirect.old_path}").exists():
|
||||
try:
|
||||
new_redirect = Redirect.objects.create(
|
||||
old_path=f"/{locale}{redirect.old_path}",
|
||||
site_id=redirect.site_id,
|
||||
is_permanent=redirect.is_permanent,
|
||||
redirect_page_id=redirect.redirect_page_id,
|
||||
redirect_page_route_path=redirect.redirect_page_route_path,
|
||||
redirect_link=redirect.redirect_link,
|
||||
)
|
||||
print(f"creating new redirect for {redirect.old_path}")
|
||||
print(f"new redirect old_path: /{locale}{redirect.old_path}")
|
||||
print(f" new redirect page_id: {redirect.redirect_page_id}")
|
||||
print(f" new redirect redirect_page_route_path: {redirect.redirect_page_route_path}")
|
||||
print(f" new redirect redirect_link: {redirect.redirect_link}")
|
||||
new_redirect.save()
|
||||
except IntegrityError:
|
||||
print(f"Redirect already exists: {redirect.old_path}")
|
||||
pass
|
||||
|
||||
redirects = Redirect.objects.all()
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -187,7 +187,7 @@ SOCIAL_SIGNIN = SOCIAL_AUTH_GOOGLE_OAUTH2_KEY is not None and SOCIAL_AUTH_GOOGLE
|
|||
USE_S3 = env("USE_S3")
|
||||
|
||||
# Detect if Django is running normally, or in test mode through "manage.py test"
|
||||
TESTING = "test" in sys.argv
|
||||
TESTING = "test" in sys.argv or "pytest" in sys.argv
|
||||
|
||||
INSTALLED_APPS = list(
|
||||
filter(
|
||||
|
@ -353,6 +353,7 @@ TEMPLATES = [
|
|||
"card_tags": "networkapi.wagtailpages.templatetags.card_tags",
|
||||
"class_tags": "networkapi.wagtailpages.templatetags.class_tags",
|
||||
"debug_tags": "networkapi.wagtailpages.templatetags.debug_tags",
|
||||
"formassembly_helper": "networkapi.utility.templatetags.formassembly_helper",
|
||||
"mofo_common": "networkapi.utility.templatetags.mofo_common",
|
||||
"homepage_tags": "networkapi.wagtailpages.templatetags.homepage_tags",
|
||||
"localization": "networkapi.wagtailpages.templatetags.localization",
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
{% extends "partials/footer.html" %}
|
||||
|
||||
{% load static i18n %}
|
||||
|
||||
{% block footer_inner_container_class %}
|
||||
{% endblock %}
|
||||
|
||||
{% block footer_donate %}
|
||||
{% endblock %}
|
||||
|
||||
{% block left_footer_column %}
|
||||
<div class="medium:tw-w-6/12 tw-px-8 tw-flex tw-flex-col tw-justify-between">
|
||||
<a class="logo tw-block tw-mb-10 meidum:tw-my-24 medium:tw-my-0" href="https://foundation.mozilla.org">
|
||||
<img src="{% static "_images/mozilla-block-white.svg" %}" width="96" height="27" alt="{% trans "Mozilla Foundation" %}">
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block right_footer_column %}
|
||||
<div class="medium:tw-w-6/12 tw-px-8 tw-flex tw-flex-col">
|
||||
<div class="tw-row">
|
||||
<div class="tw-w-full tw-px-8">
|
||||
<p class="tw-text-xs tw-my-0">
|
||||
{% blocktrans with foundation_website_url='https://foundation.mozilla.org' foundation_website='foundation.mozilla.org' cc_website_url='https://foundation.mozilla.org/about/website-licensing/' trimmed %}
|
||||
Mozilla is a global non-profit dedicated to putting you in control of your online experience and shaping the future of the web for the public good. Visit us at <a class="link" href="{{ foundation_website_url }}">{{ foundation_website }}</a>. Most content available under a <a class="link" href="{{ cc_website_url }}">Creative Commons license</a>.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,27 @@
|
|||
{% load i18n %}
|
||||
|
||||
<p class="tw-text-xs">
|
||||
{% blocktrans with stripe_url='https://stripe.com/en-ca/legal/privacy-center' paypal_url='https://www.paypal.com/us/webapps/mpp/ua/privacy-full' privacy_url="https://www.mozilla.org/privacy/websites/" trimmed %}
|
||||
Mozilla is committed to your privacy; please read our <a href="{{ privacy_url }}">privacy policy here</a>. Your payment details will be processed by <a href="{{ stripe_url }}">Stripe</a>, or <a href="{{ paypal_url }}">PayPal</a>, and a record of your donation will be stored by Mozilla.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p class="tw-text-xs">
|
||||
{% blocktrans with check_url='/ways-to-give#check' trimmed %}
|
||||
<b>Other ways to give: <a href="{{ check_url }}">Check</a></b>
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p class="tw-text-xs">
|
||||
{% blocktrans with faq_url='/faq' trimmed %}
|
||||
<b>Question donating?</b> Visit our <a href="{{ faq_url }}">FAQ</a> for answers to most common questions.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p class="tw-text-xs">
|
||||
{% blocktrans with help_url='/help/' trimmed %}
|
||||
<b>Need to reach us about your donation?</b> <a href="{{ help_url }}">Contact us</a>.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p class="tw-text-xs">
|
||||
{% blocktrans with mozilla_url='https://foundation.mozilla.org/who-we-are/' trimmed %}
|
||||
Contributions go to the <a href="{{ mozilla_url }}">Mozilla Foundation</a>, a 501(c)(3) organization based in San Francisco, California, to be used in its discretion for its charitable purposes. They are tax-deductible in the U.S. to the fullest extent permitted by law.
|
||||
{% endblocktrans %}
|
||||
</p>
|
|
@ -0,0 +1,4 @@
|
|||
<div class="tw-flex tw-justify-center">
|
||||
<a href="#XFJLGDNG" class="tw-hidden"></a>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{% load i18n primary_active_nav %}
|
||||
|
||||
{% trans "Home" as translated_root_name %}
|
||||
{% with root_name=translated_root_name %}
|
||||
<div class=" tw-bg-white tw-border-b-gray-20 tw-border-b-[1px]">
|
||||
<nav class="tw-container medium:tw-py-8">
|
||||
<div class="tw-row">
|
||||
<div class="tw-w-full tw-px-8 tw-flex tw-flex-row tw-justify-between tw-items-center">
|
||||
<div class="tw-py-9 small:tw-py-[22px] medium:tw-py-[9px]">
|
||||
<div class="tw-flex tw-items-center tw-flex-wrap">
|
||||
<a href="{{ menu_root.url }}"
|
||||
class="logo tw-text-hide tw-leading-[0] tw-bg-no-repeat tw-bg-contain
|
||||
tw-w-14 tw-h-14 tw-bg-[url('../_images/mozilla-m.svg')]
|
||||
small:tw-w-[97px] small:tw-bg-[url('../_images/mozilla-on-black.svg')]
|
||||
"
|
||||
>
|
||||
{{ root_name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
{% endwith %}
|
|
@ -0,0 +1,51 @@
|
|||
{% extends "pages/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block html_class %} tw-bg-black tw-scroll-smooth {% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
<link rel="stylesheet" href="{% static "_css/donate.compiled.css" %}">
|
||||
{% if debug %}<link rel="stylesheet" href="{% static "_css/tailwind.compiled.css" %}">{% endif %}
|
||||
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Nunito+Sans:400,300,700,300i,800,900,400i">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Zilla+Slab:300,400,500,600,700,300i,400i,600i">
|
||||
{% endblock %}
|
||||
|
||||
{% block extended_head %}{% endblock %}
|
||||
|
||||
{% block icons %}
|
||||
{% include "fragments/favicons.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block hreflang %}
|
||||
{% include "fragments/canonical_url.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block org_schema %}{% endblock %}
|
||||
|
||||
{% block additional_head_elements %}{% endblock %}
|
||||
{% if request.in_preview_panel %}
|
||||
<base target="_blank">
|
||||
{% endif %}
|
||||
|
||||
{% block bodyclass %} donate tw-m-0 tw-font-sans tw-font-normal tw-text-base tw-font-normal tw-text-black tw-bg-white tw-scroll-smooth {% endblock %}
|
||||
|
||||
{% block donate_banner %}{% endblock %}
|
||||
|
||||
{% block sticky_top_class %} tw-sticky tw-z-[1020] tw-top-0 {% endblock %}
|
||||
|
||||
{% block primary_nav %}
|
||||
{% include "../fragments/nav.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block header_wrapped %}
|
||||
{% endblock %}
|
||||
|
||||
{% block footer_block %}
|
||||
{% include "../fragments/footer.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block script_bundle %}
|
||||
<script src="{% url "javascript-catalog" %}"></script>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
{% extends "../pages/base.html" %}
|
||||
{% load wagtailcore_tags wagtailimages_tags i18n %}
|
||||
|
||||
{% block body_id %}landing{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="tw-container tw-my-[50px]">
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-8">
|
||||
<div class="tw-col-span-12 medium:tw-col-span-6">
|
||||
<picture class="tw-w-full">
|
||||
{% image page.featured_image fill-290x95 as imageMobile %}
|
||||
{% image page.featured_image fill-580x190 as imageMobile_2x %}
|
||||
{% image page.featured_image width-350 as imageTablet %}
|
||||
{% image page.featured_image width-700 as imageTablet_2x %}
|
||||
{% image page.featured_image width-690 as imageDesktop %}
|
||||
{% image page.featured_image width-1380 as imageDesktop_2x %}
|
||||
<source media="(min-width: 1201px)" srcset="{{ imageDesktop.url }}, {{ imageDesktop_2x.url }} 2x">
|
||||
<source media="(min-width: 600px)" srcset="{{ imageTablet.url }}, {{ imageTablet_2x.url }} 2x">
|
||||
<source srcset="{{ imageMobile.url }}, {{ imageMobile_2x.url }} 2x">
|
||||
|
||||
<img src="{{ imageMobile.url }}" class="tw-w-full tw-mb-8" alt="{% trans "featured image" %}">
|
||||
</picture>
|
||||
<h1 class="medium:tw-hidden">{{ page.title }}</h1>
|
||||
<div class="intro">
|
||||
{{ page.intro|richtext }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-col-span-12 medium:tw-col-span-6">
|
||||
<div class="medium:tw-w-[330px] large:tw-w-[380px] tw-mx-auto">
|
||||
<h1 class="tw-mt-0 tw-pl-[28px] tw-hidden medium:tw-inline-block">{{ page.title }}</h1>
|
||||
{% include "../fragments/fundraise_up_form.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-row tw-my-12">
|
||||
<div class="tw-w-full tw-px-8">
|
||||
{% include "../fragments/fundraise_up_disclaimer.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
<div id="breadcrumb-container" class="tw-pr-8">
|
||||
<div class="tw-flex tw-flex-wrap">
|
||||
{% for breadcrumb in breadcrumb_list %}
|
||||
{% with breadcrumb_classes="tw-h4-heading tw-px-2 tw-mb-0 tw-text-base large:tw-text-lg" %}
|
||||
<h4 class="{{ breadcrumb_classes }}">
|
||||
<a href="{{ breadcrumb.url }}">{{ breadcrumb.title }}</a>
|
||||
<span class="tw-pl-2"> / </span>
|
||||
</h4>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,14 @@
|
|||
{% extends "partials/footer.html" %}
|
||||
|
||||
{% load i18n l10n static %}
|
||||
|
||||
{% block general_links %}
|
||||
<li class="tw-mb-4"><a id="donate-footer-btn" href="?form=donate&utm_medium=FMO&utm_source=PNI&utm_campaign=23-PNI&utm_content=footer&c_id=7014x000000eQOD" class="tw-dark tw-text-white hover:tw-text-blue-20 hover:tw-underline">{% trans "Donate" %}</a></li>
|
||||
<!-- the rest of the links should be listed alphabetically -->
|
||||
<li class="tw-mb-4"><a href="https://careers.mozilla.org/listings/?team=Mozilla%20Foundation" class="tw-dark tw-text-white hover:tw-text-blue-20 hover:tw-underline">{% trans "Careers" %}</a></li>
|
||||
<li class="tw-mb-4"><a href="https://www.mozilla.org/privacy/websites/#cookies" class="tw-dark tw-text-white hover:tw-text-blue-20 hover:tw-underline">{% trans "Cookies" %}</a></li>
|
||||
<li class="tw-mb-4"><a href="https://www.mozilla.org/about/legal/terms/mozilla/" class="tw-dark tw-text-white hover:tw-text-blue-20 hover:tw-underline">{% trans "Legal" %}</a></li>
|
||||
<li class="tw-mb-4"><a href="https://www.mozilla.org/about/governance/policies/participation/" class="tw-dark tw-text-white hover:tw-text-blue-20 hover:tw-underline">{% trans "Participation Guidelines" %}</a></li>
|
||||
<li class="tw-mb-4"><a href="https://foundation.mozilla.org/en/press-center/" class="tw-dark tw-text-white hover:tw-text-blue-20 hover:tw-underline">{% trans "Press Center" %}</a></li>
|
||||
<li ><a href="https://mozilla.org/privacy/websites/" class="tw-dark tw-text-white hover:tw-text-blue-20 hover:tw-underline">{% trans "Privacy" %}</a></li>
|
||||
{% endblock general_links %}
|
|
@ -14,5 +14,5 @@
|
|||
{% endwith %}
|
||||
{% localizedroutablepageurl home_page 'about-why-view' as about_why_url %}
|
||||
<a class="{{ class }} {% bg_active_nav request.get_full_path about_why_url %}" href="{{ about_why_url }}">{% trans "About" %}</a>
|
||||
<a id="donate-header-btn" class="{{ class }}" href="?form=donate">{% trans "Donate" %}</a>
|
||||
<a id="donate-header-btn" class="{{ class }}" href="?form=donate&utm_medium=FMO&utm_source=PNI&utm_campaign=23-PNI&utm_content=header&c_id=7014x000000eQOD">{% trans "Donate" %}</a>
|
||||
{{ post }}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{% load static i18n wagtailimages_tags %}
|
||||
|
||||
{% with image_classes="-tw-ml-[0.7rem] tw-overflow-hidden tw-rounded-full tw-border-2 tw-border-white tw-inline-block tw-w-20 tw-h-20 tw-border-r d-flex align-items-center justify-content-center last:tw-mr-4" no_image_classes="tw-bg-[url('../_images/blog-author-placeholder.jpg')] tw-bg-cover " %}
|
||||
{% with image_classes="-tw-ml-[0.7rem] tw-overflow-hidden tw-rounded-full tw-border-2 tw-border-white tw-inline-block tw-w-20 tw-h-20 tw-border-r d-flex align-items-center justify-content-center last:tw-mr-4" no_image_classes="tw-bg-[url('../_images/author-placeholder.jpg')] tw-bg-cover " %}
|
||||
<div class="profile-images d-flex flex-wrap tw-pl-[0.7rem] tw-z-10">
|
||||
{% for profile in profiles %}
|
||||
<div class="profile-image {{ image_classes }} {% if not profile.image %} {{ no_image_classes }} {% endif %}" aria-label="{{ profile.name }}" style="z-index:-{{ forloop.counter }};">
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
{% load static wagtailroutablepage_tags wagtailimages_tags %}
|
||||
<a
|
||||
href="{% routablepageurl page=authors_index.localized profile_slug=author_profile.slug url_name="wagtailpages:research-author-detail" %}"
|
||||
class="tw-flex {% if not tight %} tw-gap-8 medium:tw-gap-12 large:tw-gap-16 {% else %} tw-gap-6 {% endif %} tw-group tw-items-center hover:tw-no-underline"
|
||||
>
|
||||
<div class="tw-shrink-0 tw-w-32 {% if not tight %} medium:tw-w-40 large:tw-w-48 {% endif %}">
|
||||
{% with profile_image_classes="tw-w-full tw-h-auto tw-rounded-full" %}
|
||||
{% if author_profile.image %}
|
||||
{% image author_profile.image fill-182x182 as profile_img %}
|
||||
<img src="{{ profile_img.url }}" alt="{{ author_profile.name }}" class="{{ profile_image_classes }}">
|
||||
{% else %}
|
||||
<img src="{% static '_images/author-placeholder.jpg' %}" alt="{{ author_profile.name }}" class="{{ profile_image_classes }}">
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div>
|
||||
<div class="tw-h5-heading tw-mb-2 tw-text-blue-80 group-hover:tw-underline">
|
||||
{{ author_profile.name }}
|
||||
</div>
|
||||
<div class="tw-text-xs medium:tw-text-sm large:tw-text-base tw-text-gray-60">
|
||||
{{ author_profile.tagline }}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
|
@ -0,0 +1,16 @@
|
|||
{% load l10n %}
|
||||
{% for field in form %}
|
||||
{% if field|length %}
|
||||
<div class="tw-pt-8 tw-pb-24 tw-border-t">
|
||||
<fieldset>
|
||||
<legend>
|
||||
<h3 class="tw-h4-heading">{{ field.label_tag }}</h3>
|
||||
</legend>
|
||||
{{ field.errors }}
|
||||
<ul class="tw-list-none tw-mb-0 tw-pl-0 {% if field|length > 5 %} tw-max-h-[13.5rem] tw-overflow-y-scroll{% endif %}">
|
||||
{% for option in field %}<li class="tw-flex tw-flex-row tw-items-baseline">{{ option }}</li>{% endfor %}
|
||||
</ul>
|
||||
</fieldset>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
|
@ -3,7 +3,7 @@
|
|||
{% get_current_language as lang_code %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ lang_code }}">
|
||||
<html lang="{{ lang_code }}" class="{% block html_class %}{% endblock %}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>
|
||||
|
@ -98,7 +98,7 @@
|
|||
|
||||
{% block body_wrapped %}
|
||||
<div class="wrapper">
|
||||
<div class="sticky-top d-print-none">
|
||||
<div class="{% block sticky_top_class %} sticky-top {% endblock %} d-print-none">
|
||||
{% block primary_nav %}
|
||||
{% include "partials/primary_nav.html" with background="simple-background" %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -104,7 +104,7 @@
|
|||
{% block background_parallax %}
|
||||
{% endblock %}
|
||||
<div class="tw-relative tw-z-10">
|
||||
{% include "partials/footer.html" %}
|
||||
{% include "fragments/buyersguide/footer.html" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -117,6 +117,24 @@
|
|||
tw-gap-1
|
||||
tw-grid-flow-row-dense
|
||||
">
|
||||
{% if featured_cta %}
|
||||
{% comment %}
|
||||
We render the featured CTA only once into the markup if one exists.
|
||||
The categories only have a toggle to define if the featured CTA should be shown when the page is filtered for the category.
|
||||
The "current category" is the one the page is first loaded with. We show the CTA immediately if that category would, otherwise the CTA is initially hidden.
|
||||
JS is used to handle the show and hide of the CTA when the active category is changed. The data attribute contains the information for which categories the CTA should be shown.
|
||||
{% endcomment %}
|
||||
<div
|
||||
id="category-featured-cta"
|
||||
class="tw-col-span-2 tw-flex tw-flex-row tw-w-full {% if not current_category.show_cta %} tw-hidden {% endif %}"
|
||||
data-show-for-categories="{% for category in categories %}{% if category.show_cta %}{{ category.name }}, {% endif %}{% endfor %}"
|
||||
>
|
||||
{% with cta=featured_cta %}
|
||||
{% include "fragments/buyersguide/call_to_action_box.html" with icon=cta.sticker_image heading=cta.title body=cta.content link_text=cta.link_label link_href=cta.get_target_url large=True %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% block extra_product_box_list_items %}{% endblock extra_product_box_list_items %}
|
||||
|
||||
{% if request.user.is_anonymous %}
|
||||
|
|
|
@ -4,24 +4,6 @@
|
|||
{% block body_id %}{{ category.slug }}{% endblock %}
|
||||
|
||||
{% block extra_product_box_list_items %}
|
||||
{% if featured_cta %}
|
||||
{% comment %}
|
||||
We render the featured CTA only once into the markup if one exists.
|
||||
The categories only have a toggle to define if the featured CTA should be shown when the page is filtered for the category.
|
||||
The "current category" is the one the page is first loaded with. We show the CTA immediately if that category would, otherwise the CTA is initially hidden.
|
||||
JS is used to handle the show and hide of the CTA when the active category is changed. The data attribute contains the information for which categories the CTA should be shown.
|
||||
{% endcomment %}
|
||||
<div
|
||||
id="category-featured-cta"
|
||||
class="tw-col-span-2 tw-flex tw-flex-row tw-w-full {% if not current_category.show_cta %} tw-hidden {% endif %}"
|
||||
data-show-for-categories="{% for category in categories %}{% if category.show_cta %}{{ category.name }}, {% endif %}{% endfor %}"
|
||||
>
|
||||
{% with cta=featured_cta %}
|
||||
{% include "fragments/buyersguide/call_to_action_box.html" with icon=cta.sticker_image heading=cta.title body=cta.content link_text=cta.link_label link_href=cta.get_target_url large=True %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for category in categories %}
|
||||
{% comment %}
|
||||
Each category has it's own set of related articles.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "./research_base.html" %}
|
||||
{% load i18n l10n wagtailcore_tags wagtailimages_tags %}
|
||||
{% extends "./base.html" %}
|
||||
{% load i18n l10n wagtailcore_tags wagtailimages_tags breadcrumbs %}
|
||||
|
||||
{% block research_hub_content %}
|
||||
<div
|
||||
|
@ -20,7 +20,7 @@
|
|||
class="medium:tw-col-start-2"
|
||||
{% endif %}
|
||||
>
|
||||
{% include './fragments/research_breadcrumb.html' with breadcrumb_list=breadcrumbs %}
|
||||
{% get_research_breadcrumbs include_self=True %}
|
||||
</div>
|
||||
{% if author_profile.image %}
|
||||
<div id="image-container" class="tw-place-self-center medium:tw-place-self-start">
|
||||
|
@ -56,12 +56,12 @@
|
|||
<ul class="tw-list-none px-0">
|
||||
{% for research_detail_page in latest_research %}
|
||||
<li class="tw-border-t tw-border-t-gray-20 tw-mt-12 tw-pt-12">
|
||||
{% include "wagtailpages/fragments/research_detail_card.html" with research_detail_page=research_detail_page hide_author_names=True hide_image_on_mobile=True %}
|
||||
{% include "fragments/research_hub/detail_card.html" with research_detail_page=research_detail_page hide_author_names=True hide_image_on_mobile=True %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if author_research_count > 3 %}
|
||||
<a href="{% pageurl library_page %}?author={{ author_profile.id|unlocalize }}" class="tw-btn-secondary tw-mt-12 small:tw-mt-16 tw-w-full small:tw-w-auto">{% translate 'Browse all projects' context 'Button to see more than the latest three elements of research from an author' %} ({{ author_research_count }})</a>
|
||||
<a href="{% pageurl library_page %}?author={{ author_profile.id|unlocalize }}" class="tw-btn-secondary tw-mt-12 small:tw-mt-16 tw-w-full small:tw-w-auto">{% translate "Browse all projects" context "Button to see more than the latest three elements of research from an author" %} ({{ author_research_count }})</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
|
@ -1,9 +1,9 @@
|
|||
{% extends "./research_base.html" %}
|
||||
{% load i18n static %}
|
||||
{% extends "./base.html" %}
|
||||
{% load i18n static breadcrumbs %}
|
||||
|
||||
{% block research_hub_content %}
|
||||
<div>
|
||||
{% include './fragments/research_breadcrumb.html' with breadcrumb_list=breadcrumbs %}
|
||||
{% get_research_breadcrumbs %}
|
||||
<h1>{{ page.title }}</h1>
|
||||
</div>
|
||||
|
||||
|
@ -13,7 +13,7 @@
|
|||
<ul class="tw-grid medium:tw-grid-cols-2 medium:tw-gap-16 tw-gap-12 tw-gap-y-24 tw-list-none tw-p-0">
|
||||
{% for author_profile in author_profiles %}
|
||||
<li class="tw-flex tw-m-0">
|
||||
{% include "wagtailpages/fragments/research_author_card.html" with authors_index=page author_profile=author_profile %}
|
||||
{% include "fragments/research_hub/author_card.html" with authors_index=page author_profile=author_profile %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "./modular_page.html" %}
|
||||
{% extends "wagtailpages/modular_page.html" %}
|
||||
|
||||
{% block hero_guts %}
|
||||
{% include "wagtailpages/fragments/research_hero_guts.html" with banner_image=page.get_banner %}
|
||||
{% include "fragments/research_hub/hero_guts.html" with banner_image=page.get_banner %}
|
||||
{% endblock %}
|
||||
|
||||
{% block subcontent %}
|
|
@ -1,11 +1,11 @@
|
|||
{% extends "./research_base.html" %}
|
||||
{% load i18n wagtailcore_tags wagtailimages_tags %}
|
||||
{% extends "./base.html" %}
|
||||
{% load i18n wagtailcore_tags wagtailimages_tags breadcrumbs %}
|
||||
|
||||
{% block research_hub_content %}
|
||||
<div class="tw-grid tw-grid-cols-1 tw-grid-rows-[auto_auto_1fr] medium:tw-grid-cols-[12rem_1fr_14rem] large:tw-grid-cols-[14rem_1fr_16rem] xlarge:tw-grid-cols-[14rem_1fr_16rem] tw-gap-12">
|
||||
|
||||
<div id="breadcrumbs-container" class="medium:tw-col-start-2 medium:tw-col-end-4">
|
||||
{% include "wagtailpages/fragments/research_breadcrumb.html" with breadcrumb_list=breadcrumbs %}
|
||||
{% get_research_breadcrumbs %}
|
||||
</div>
|
||||
|
||||
<div id="title-and-meta" class="medium:tw-col-start-2 medium:tw-col-end-4 xlarge:tw-col-end-3 tw-min-w-0">
|
||||
|
@ -57,13 +57,13 @@
|
|||
</div>
|
||||
|
||||
<div id="authors-and-collaborators" class="medium:tw-col-start-3 medium:tw-col-end-4 tw-mt-12 medium:tw-mt-24">
|
||||
{% if page.research_authors.all %}
|
||||
{% if research_authors %}
|
||||
<div class="tw-border-t tw-border-black">
|
||||
<h2 class="tw-h4-heading tw-my-12">{% trans "Project leads" %}</h2>
|
||||
<ul class="tw-p-0 tw-list-none">
|
||||
{% for research_author in page.research_authors.all %}
|
||||
{% for author in research_authors %}
|
||||
<li class="tw-mb-12">
|
||||
{% include "wagtailpages/fragments/research_author_card.html" with authors_index=authors_index author_profile=research_author.author_profile tight=True %}
|
||||
{% include "fragments/research_hub/author_card.html" with authors_index=authors_index author_profile=author tight=True %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "./research_base.html" %}
|
||||
{% extends "./base.html" %}
|
||||
{% load i18n l10n wagtailcore_tags wagtailimages_tags static %}
|
||||
|
||||
{% block hero_guts %}
|
||||
|
@ -26,7 +26,7 @@
|
|||
{% endif %}
|
||||
<form id="search" action="{% pageurl library_page %}" method="get" accept-charset="utf-8" class="tw-my-12 tw-w-full tw-flex tw-flex-col medium:tw-flex-row tw-gap-8 medium:tw-gap-8 tw-items-start medium:tw-items-center">
|
||||
<div class="tw-w-full medium:tw-w-3/4">
|
||||
{% include "wagtailpages/fragments/research_search_bar.html" %}
|
||||
{% include "fragments/research_hub/search_bar.html" %}
|
||||
</div>
|
||||
<a href="{% pageurl library_page %}" class="tw-block tw-font-bold">
|
||||
{% trans "Browse all" context "Button" %}
|
||||
|
@ -39,7 +39,7 @@
|
|||
<ul class="tw-list-none px-0 ">
|
||||
{% for research_detail_page in latest_research_detail_pages %}
|
||||
<li class="tw-py-8 small:tw-my-8 small:tw-bg-gray-05 small:tw-p-16">
|
||||
{% include "wagtailpages/fragments/research_detail_card.html" with research_detail_page=research_detail_page hide_related_topics=True landing_page=True %}
|
||||
{% include "fragments/research_hub/detail_card.html" with research_detail_page=research_detail_page hide_related_topics=True landing_page=True %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
|
@ -1,14 +1,11 @@
|
|||
{% extends "./research_base.html" %}
|
||||
{% load i18n static wagtailcore_tags wagtailimages_tags %}
|
||||
|
||||
|
||||
{% extends "./base.html" %}
|
||||
{% load i18n static wagtailcore_tags wagtailimages_tags breadcrumbs %}
|
||||
{% block research_hub_content %}
|
||||
<div>
|
||||
{% include './fragments/research_breadcrumb.html' with breadcrumb_list=breadcrumbs %}
|
||||
{% get_research_breadcrumbs %}
|
||||
<h1>{{ page.title }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="large:tw-w-160 large:tw-h-64 tw-mt-12 tw-mb-8 large:tw-mb-0 large:-tw-ml-12 large:tw-p-12 large:tw-bg-gray-05" >
|
||||
<div class="large:tw-w-160 large:tw-h-64 tw-mt-12 tw-mb-8 large:tw-mb-0 large:-tw-ml-12 large:tw-p-12 large:tw-bg-gray-05">
|
||||
{# SEARCH BAR #}
|
||||
{% comment %}
|
||||
The page url is necessary in the form to that the filter anchor link is not carried forward.
|
||||
|
@ -16,37 +13,43 @@
|
|||
that adds the `#filter` part to the URL. Submitting the form in that stage without the
|
||||
explicit URL would carry the anchor link forward. That would mean the view is scrolled to the
|
||||
filter section again upon page reload. This seems undesireable.
|
||||
{% endcomment %}
|
||||
<form action="{% pageurl page %}" method="get" accept-charset="utf-8" id="search-form">
|
||||
{% include "wagtailpages/fragments/research_search_bar.html" %}
|
||||
{% endcomment %}
|
||||
<form action="{% pageurl page %}"
|
||||
method="get"
|
||||
accept-charset="utf-8"
|
||||
id="search-form">
|
||||
{% include "fragments/research_hub/search_bar.html" %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="tw-flex tw-flex-col large:tw-flex-row-reverse large:tw-gap-16">
|
||||
{# FILTER, SORT AND RESULTS #}
|
||||
|
||||
{# For side-by-side layout, we need to pull the results up to that the upper end lines up with the search bar. #}
|
||||
<div class="tw-min-w-0 tw-grow large:-tw-mt-64 tw-pb-24">
|
||||
{# SORT AND RESULTS #}
|
||||
|
||||
|
||||
<div class="tw-flex tw-flex-col large:tw-flex-row-reverse large:tw-justify-between tw-gap-12">
|
||||
<div class="tw-flex tw-flex-row tw-gap-6">
|
||||
{# FILTER BUTTON #}
|
||||
<div class="large:tw-hidden tw-basis-1/2">
|
||||
{% include "wagtailpages/fragments/research_filter_button.html" with button=False %}
|
||||
{% include "wagtailpages/fragments/research_filter_button.html" with button=True %}
|
||||
{% include "fragments/research_hub/filter_button.html" with button=False %}
|
||||
{% include "fragments/research_hub/filter_button.html" with button=True %}
|
||||
</div>
|
||||
<div class="tw-flex tw-flex-row tw-items-baseline tw-basis-1/2 large:tw-basis-full">
|
||||
{# SORT SELECT #}
|
||||
<select id='sort-select' name="sort" class="tw-form-control tw-border-gray-40" form="search-form">
|
||||
<select id='sort-select'
|
||||
name="sort"
|
||||
class="tw-form-control tw-border-gray-40"
|
||||
form="search-form">
|
||||
{% for choice in page.SORT_CHOICES.values %}
|
||||
<option value="{{ choice.value }}" {% if choice == sort %}selected{% endif %}>{{ choice.label }}</option>
|
||||
<option value="{{ choice.value }}" {% if choice == sort %}selected{% endif %}>
|
||||
{{ choice.label }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<noscript>
|
||||
{# The sort button is only needed for the no JS case. With JS, the form can be submitted on change of the select #}
|
||||
<button type="submit" class="tw-btn-primary tw-text-base" form="search-form">{% translate 'Sort' context 'Button' %}</button>
|
||||
<button type="submit" class="tw-btn-primary tw-text-base" form="search-form">
|
||||
{% translate 'Sort' context 'Button' %}
|
||||
</button>
|
||||
</noscript>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -67,12 +70,11 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="tw-list-none tw-mt-16 large:tw-mt-12 tw-mb-12 tw-px-0 tw-border-t tw-border-b tw-border-gray-20 tw-divide-y tw-divide-gray-05">
|
||||
{# RESULTS LIST #}
|
||||
{% for research_detail_page in research_detail_pages %}
|
||||
<li class="tw-m-0 tw-pt-12 tw-pb-12">
|
||||
{% include "wagtailpages/fragments/research_detail_card.html" with research_detail_page=research_detail_page hide_image_on_mobile=True hide_related_topics_on_mobile=True %}
|
||||
{% include "fragments/research_hub/detail_card.html" with research_detail_page=research_detail_page hide_image_on_mobile=True hide_related_topics_on_mobile=True %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
@ -81,68 +83,34 @@
|
|||
{% include "fragments/pagination.html" with page=research_detail_pages %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="filter" class="
|
||||
tw-bg-gray-05
|
||||
large:tw-block
|
||||
large:tw-mr-0
|
||||
large:-tw-ml-12
|
||||
large:tw-overflow-y-clip
|
||||
tw-pt-8 large:tw-pt-0
|
||||
tw-px-8 small:tw-px-12 medium:tw-px-16 large:tw-px-12
|
||||
tw-shrink-0
|
||||
large:tw-w-160
|
||||
">
|
||||
<div id="filter"
|
||||
class=" tw-bg-gray-05 large:tw-block large:tw-mr-0 large:-tw-ml-12 large:tw-overflow-y-clip tw-pt-8 large:tw-pt-0 tw-px-8 small:tw-px-12 medium:tw-px-16 large:tw-px-12 tw-shrink-0 large:tw-w-160 ">
|
||||
{# FILTER SECTION #}
|
||||
<div class="tw-flex tw-justify-end">
|
||||
<button
|
||||
id="filter-section-hide-button"
|
||||
<button id="filter-section-hide-button"
|
||||
class="tw-hidden large:tw-hidden tw-h-24 tw-w-24 -tw-mt-4 -tw-mr-4 -tw-mb-8 tw-text-3xl tw-font-normal tw-text-blue-80 hover:tw-text-blue-20 tw-bg-transparent"
|
||||
aria-label="{% translate "Close" %}"
|
||||
tabIndex="0"
|
||||
>
|
||||
tabIndex="0">
|
||||
<span aria-hidden="true" class="">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h2 class="large:tw-hidden tw-h1-heading">{% translate 'Filter' %}</h2>
|
||||
|
||||
{% if topic_options %}
|
||||
{% translate 'Topics' as heading %}
|
||||
{% include "wagtailpages/fragments/research_filter_group.html" with heading=heading options=topic_options|dictsort:'label' checked_option_values=filtered_topic_ids field_name='topic' %}
|
||||
{% endif %}
|
||||
|
||||
{% if year_options %}
|
||||
{% translate 'Publication date' as heading %}
|
||||
{% include "wagtailpages/fragments/research_filter_group.html" with heading=heading radio=True options=year_options checked_option_value=filtered_year field_name='year' %}
|
||||
{% endif %}
|
||||
|
||||
{% if author_options %}
|
||||
{% translate 'Authors' as heading %}
|
||||
{% include "wagtailpages/fragments/research_filter_group.html" with heading=heading options=author_options|dictsort:'label' checked_option_values=filtered_author_ids field_name='author' %}
|
||||
{% endif %}
|
||||
|
||||
{% if region_options %}
|
||||
{% translate 'Regions' as heading %}
|
||||
{% include "wagtailpages/fragments/research_filter_group.html" with heading=heading options=region_options|dictsort:'label' checked_option_values=filtered_region_ids field_name='region' %}
|
||||
{% endif %}
|
||||
|
||||
<div class="
|
||||
tw-bg-gray-05
|
||||
tw-bottom-0
|
||||
-tw-mx-8 small:-tw-mx-12 medium:-tw-mx-16 large:-tw-mx-12
|
||||
tw-pb-16 large:tw-pb-12
|
||||
tw-px-8 small:tw-px-12 medium:tw-px-16 large:tw-px-12
|
||||
tw-sticky
|
||||
">
|
||||
<div class="tw-pt-12 large:tw-pt-8 tw-border-t tw-border-t-gray-20">
|
||||
<button type="submit" class="tw-w-full tw-btn-primary" form="search-form">{% translate 'Apply filters' context 'Button' %}</button>
|
||||
<form action="{% pageurl page %}"
|
||||
method="get"
|
||||
accept-charset="utf-8"
|
||||
id="filter-form">
|
||||
{% include "fragments/research_library_form.html" with form=form %}
|
||||
<div class=" tw-bg-gray-05 tw-bottom-0 -tw-mx-8 small:-tw-mx-12 medium:-tw-mx-16 large:-tw-mx-12 tw-pb-16 large:tw-pb-12 tw-px-8 small:tw-px-12 medium:tw-px-16 large:tw-px-12 tw-sticky ">
|
||||
<div class="tw-pt-12 large:tw-pt-8 tw-border-t tw-border-t-gray-20">
|
||||
<button type="submit" class="tw-w-full tw-btn-primary" form="filter-form">
|
||||
{% translate 'Apply filters' context 'Button' %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock research_hub_content %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
{{ block.super }}
|
||||
<script src="{% static "_js/research-hub-library.compiled.js" %}" async defer></script>
|
|
@ -22,7 +22,7 @@
|
|||
{% endif %}
|
||||
|
||||
<div class="site-footer tw-bg-black tw-dark {{ wrapper_class }} print:tw-hidden">
|
||||
<div class="tw-container tw-mt-12">
|
||||
<div class="tw-container {% block footer_inner_container_class %} tw-mt-12 {% endblock %}">
|
||||
<div class="tw-row">
|
||||
|
||||
{% block left_footer_column %}
|
||||
|
|
|
@ -83,4 +83,4 @@ class TestApplePayDomainAssociationView(TestCase):
|
|||
response = self.client.get(self.view_url)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.content.decode(), "Request site is not recognized.")
|
||||
self.assertEqual(response.content.decode(), "Request site not recognized.")
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
from django import template
|
||||
from django.utils.translation import get_language
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag(name="fa_locale_code")
|
||||
def fa_locale_code():
|
||||
"""
|
||||
Returns the FormAssembly locale code for the current language.
|
||||
"""
|
||||
|
||||
fa_default = "en_US"
|
||||
|
||||
# key: available locales on fo.mo
|
||||
# value: ISO code used by FormAssembly https://app.formassembly.com/translate
|
||||
mappings = {
|
||||
"en": fa_default,
|
||||
"de": "de",
|
||||
"es": "es",
|
||||
"fr": "fr",
|
||||
"fy-NL": None,
|
||||
"nl": "nl",
|
||||
"pl": "pl",
|
||||
"pt-BR": "pt_BR",
|
||||
"sw": None,
|
||||
}
|
||||
|
||||
fa_supported_locale = mappings.get(get_language())
|
||||
|
||||
return fa_supported_locale if fa_supported_locale else fa_default
|
|
@ -9,13 +9,6 @@ _original_get_redirect = middleware._get_redirect
|
|||
|
||||
# Then we can create a wrapper around the original logic:
|
||||
def _new_get_redirect(request, path):
|
||||
if hasattr(request, "LANGUAGE_CODE"):
|
||||
# If this path has an i18n_patterns locale prefix, remove it.
|
||||
locale_prefix = f"/{request.LANGUAGE_CODE}/"
|
||||
if path.startswith(locale_prefix):
|
||||
path = path.replace(locale_prefix, "/", 1)
|
||||
|
||||
# Then hand off processing to the original redirect logic.
|
||||
redirect = _original_get_redirect(request, path)
|
||||
|
||||
# Wagtail currently does not forward query arguments, so for
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
To address bug from Wagtail v3
|
||||
To address bug from Wagtail v3+
|
||||
See context: https://github.com/mozilla/foundation.mozilla.org/issues/10192
|
||||
*/
|
||||
body#wagtail
|
||||
|
@ -17,49 +17,3 @@ body#wagtail
|
|||
height: unset;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
To address bug from Wagtail v3
|
||||
See context: https://github.com/mozilla/foundation.mozilla.org/issues/10194
|
||||
*/
|
||||
|
||||
body#wagtail
|
||||
main#main
|
||||
form
|
||||
ul
|
||||
li
|
||||
.field
|
||||
.c-dropdown.t-inverted.c-dropdown--large
|
||||
a {
|
||||
background-color: var(--color-primary-darker);
|
||||
color: white;
|
||||
}
|
||||
|
||||
body#wagtail
|
||||
main#main
|
||||
form
|
||||
ul
|
||||
li
|
||||
.field
|
||||
.c-dropdown.t-inverted.c-dropdown--large
|
||||
.c-dropdown__item
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/*
|
||||
To address bug from Wagtail v3
|
||||
See context: https://github.com/MozillaFoundation/foundation.mozilla.org/issues/10193
|
||||
*/
|
||||
|
||||
body#wagtail.page-editor main#main aside.form-side {
|
||||
/* Override the height: 100vh rule from Wagtail source code.
|
||||
Minus 1px to account for bottom border in its ancestor div .content-wrapper
|
||||
Minus 51px to account for the height of the sticky top bar
|
||||
*/
|
||||
height: calc(100vh - 1px - 51px);
|
||||
}
|
||||
|
||||
body#wagtail.page-editor main#main.content-wrapper {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
{% extends "wagtailadmin/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load static wagtailadmin_tags %}
|
||||
|
||||
This is a customized version of https://github.com/wagtail/wagtail/blob/v2.11.3/wagtail/contrib/redirects/templates/wagtailredirects/index.html
|
||||
|
||||
{% block titletag %}{% trans "Redirects" %}{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{{ block.super }}
|
||||
<script nonce="{{ request.csp_nonce }}">
|
||||
window.headerSearch = {
|
||||
url: "{% url 'wagtailredirects:index' %}",
|
||||
termInput: "#id_q",
|
||||
targetOutput: "#redirects-results"
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'wagtailredirects/css/index.css' %}">
|
||||
|
||||
{% trans "Redirects" as redirects_str %}
|
||||
{% if user_can_add %}
|
||||
{% url "wagtailredirects:add" as add_link %}
|
||||
{% trans "Add redirect" as add_str %}
|
||||
{% url "wagtailredirects:start_import" as import_link %}
|
||||
{% trans "Import redirects" as import_str %}
|
||||
|
||||
<header class="hasform">
|
||||
{% block breadcrumb %}{% endblock %}
|
||||
<div class="row nice-padding">
|
||||
<div class="left">
|
||||
<div class="col header-title">
|
||||
<h1 class="icon icon-redirect">{{ redirects_str }}</h1>
|
||||
</div>
|
||||
<form class="col search-form" action="{% url "wagtailredirects:index" %}{% if query_parameters %}?{{ query_parameters }}{% endif %}" method="get" novalidate role="search">
|
||||
<ul class="fields">
|
||||
{% for field in search_form %}
|
||||
{% include "wagtailadmin/shared/field_as_li.html" with field=field field_classes="field-small iconfield" input_classes="icon-search" %}
|
||||
{% endfor %}
|
||||
<li class="submit visuallyhidden"><input type="submit" value="Search" class="button" /></li>
|
||||
</ul>
|
||||
</form>
|
||||
</div>
|
||||
<div class="right has-multiple-actions">
|
||||
<div class="actionbutton">
|
||||
<a href="{{ add_link }}" class="button bicolor button--icon">{% icon name="plus" wrapped=1 %}{{ add_str }}</a>
|
||||
</div>
|
||||
<div class="actionbutton">
|
||||
<a href="{{ import_link }}" class="button bicolor button--icon">{% icon name="doc-full-inverse" wrapped=1 %}{{ import_str }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{% else %}
|
||||
{% include "wagtailadmin/shared/header.html" with title=redirects_str icon="redirect" search_url="wagtailredirects:index" %}
|
||||
{% endif %}
|
||||
|
||||
<div class="nice-padding">
|
||||
<div id="redirects-explanation">
|
||||
<h1>
|
||||
How to use Redirects
|
||||
</h1>
|
||||
<p>
|
||||
Wagtail Redirects are rules for catching 404 errors, also known as "page not found" errors, and instead directing
|
||||
users to another page, or external website. Redirects in the Mozilla Foundation CMS are locale-agnostic, meaning
|
||||
that a redirect for any non-existent url <code>/some-page</code> will trigger regardless of whether it was requested
|
||||
with a locale prefix. A rule for <code>/some-page</code> will automatically kick in for <code>/en/some-page</code>,
|
||||
<code>/fr/some-page</code> etc. Also, note that redirects only work for pages that are <strong>not live</strong>.
|
||||
You cannot create a redirect for a published page: you will have to unpublish that page before a redirect will work.
|
||||
</p>
|
||||
<hr/>
|
||||
</div>
|
||||
<div id="redirects-results" class="redirects">
|
||||
{% include "wagtailredirects/results.html" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,20 +0,0 @@
|
|||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from wagtail.contrib.redirects.models import Redirect
|
||||
|
||||
|
||||
# Safeguard against the fact that static assets and views might be hosted remotely,
|
||||
# see https://docs.djangoproject.com/en/3.1/topics/testing/tools/#urlconf-configuration
|
||||
@override_settings(STATICFILES_STORAGE="django.contrib.staticfiles.storage.StaticFilesStorage")
|
||||
class LocalizedRedirectTests(TestCase):
|
||||
def setUp(self):
|
||||
redirect = Redirect(old_path="/test", redirect_link="/")
|
||||
redirect.save()
|
||||
|
||||
def test_plain_redirect(self):
|
||||
response = self.client.get("/test/", follow=True)
|
||||
self.assertEqual(response.redirect_chain, [("/", 301), ("/en/", 302)])
|
||||
|
||||
def test_localized_redirect(self):
|
||||
response = self.client.get("/en/test/", follow=True)
|
||||
self.assertEqual(response.redirect_chain, [("/", 301), ("/en/", 302)])
|
|
@ -0,0 +1,94 @@
|
|||
from django.test.utils import override_settings
|
||||
from wagtail.contrib.redirects.models import Redirect
|
||||
|
||||
from networkapi.wagtailpages.factory import buyersguide as buyersguide_factories
|
||||
from networkapi.wagtailpages.tests import base as test_base
|
||||
|
||||
|
||||
# Safeguard against the fact that static assets and views might be hosted remotely,
|
||||
# see https://docs.djangoproject.com/en/3.1/topics/testing/tools/#urlconf-configuration
|
||||
@override_settings(STATICFILES_STORAGE="django.contrib.staticfiles.storage.StaticFilesStorage")
|
||||
class LocalizedRedirectTests(test_base.WagtailpagesTestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
super().setUpTestData()
|
||||
cls.buyersguide_homepage = buyersguide_factories.BuyersGuidePageFactory(
|
||||
parent=cls.homepage,
|
||||
)
|
||||
cls.content_index = buyersguide_factories.BuyersGuideEditorialContentIndexPageFactory(
|
||||
parent=cls.buyersguide_homepage,
|
||||
)
|
||||
|
||||
def test_redirect(self):
|
||||
"""Check that we are redirected to the localized version
|
||||
of the homepage when a Redirect object exists.
|
||||
In this example, a Redirect object exists for /test/ -> /
|
||||
so we expect to be redirected to /en/ when we visit /test/
|
||||
"""
|
||||
redirect = Redirect(old_path="/test", redirect_link="/final")
|
||||
redirect.save()
|
||||
response = self.client.get("/test/", follow=True)
|
||||
|
||||
self.assertEqual(response.redirect_chain, [("/final", 301), ("/en/final/", 302)])
|
||||
|
||||
# Finally, check that the redirect doesn't work for other locales
|
||||
# since a redirect only exists for /test/ -> /final/
|
||||
response = self.client.get("/en/test/", follow=True)
|
||||
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
response = self.client.get("/fr/test/", follow=True)
|
||||
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_localized_redirect(self):
|
||||
"""Check that a Redirect with a language code in the old_path
|
||||
and no language code in the redirect_link is handled correctly.
|
||||
We expect to be redirected to /en/final/ when we visit /en/test/.
|
||||
"""
|
||||
redirect = Redirect(old_path="/en/test", redirect_link="/final")
|
||||
redirect.save()
|
||||
response = self.client.get("/final/", follow=True)
|
||||
|
||||
self.assertEqual(response.redirect_chain, [("/en/final/", 302)])
|
||||
|
||||
response = self.client.get("/en/test/", follow=True)
|
||||
|
||||
self.assertEqual(response.redirect_chain, [("/final", 301), ("/en/final/", 302)])
|
||||
|
||||
def test_localized_redirect_to_default_locale(self):
|
||||
"""Check that a Redirect with a language code in the old_path
|
||||
and no language code in the redirect_link is handled correctly.
|
||||
We expect /fr/test/ -> /final/ Should land at the default locale /en/final/
|
||||
"""
|
||||
# First ensure no redirects exist that can intefere with this test
|
||||
self.assertFalse(Redirect.objects.all())
|
||||
|
||||
redirect = Redirect(old_path="/fr/test", redirect_link="/final")
|
||||
redirect.save()
|
||||
response = self.client.get("/fr/test/", follow=True)
|
||||
|
||||
self.assertEqual(response.redirect_chain, [("/final", 301), ("/en/final/", 302)])
|
||||
|
||||
def test_fr_redirect_to_fr_target(self):
|
||||
"""Check that a Redirect with a language code in the old_path
|
||||
and language code in the redirect_link is handled correctly.
|
||||
We expect /fr/test/ -> /fr/final/ Should land at /fr/final/
|
||||
"""
|
||||
# First ensure no redirects exist that can intefere with this test
|
||||
self.assertFalse(Redirect.objects.all())
|
||||
|
||||
redirect = Redirect(old_path="/fr/test", redirect_link="/fr/final")
|
||||
redirect.save()
|
||||
response = self.client.get("/fr/test/", follow=True)
|
||||
|
||||
self.assertEqual(response.redirect_chain, [("/fr/final", 301), ("/fr/final/", 301)])
|
||||
|
||||
def test_no_redirect_does_redirect(self):
|
||||
"""Prove that a path is redirected to /en/ even if there isn't
|
||||
a Redirect object added. This is evidence that the localization
|
||||
framework is handling it.
|
||||
"""
|
||||
response = self.client.get("/no-redirect/", follow=True)
|
||||
|
||||
self.assertEqual(response.redirect_chain, [("/en/no-redirect/", 302)])
|
|
@ -0,0 +1,117 @@
|
|||
import http
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.urls import reverse
|
||||
from wagtail.contrib.redirects.models import Redirect
|
||||
from wagtail.core.models import Locale
|
||||
|
||||
from networkapi.wagtailpages.factory import buyersguide as buyersguide_factories
|
||||
from networkapi.wagtailpages.factory import publication as publication_factory
|
||||
from networkapi.wagtailpages.tests import base as test_base
|
||||
|
||||
|
||||
class PageRedirectTest(test_base.WagtailpagesTestCase):
|
||||
"""
|
||||
Test class for testing page redirects in Wagtail.
|
||||
|
||||
This class inherits from the WagtailpagesTestCase class and sets up a test
|
||||
environment to test page redirects. The class contains three test methods:
|
||||
1. test_page_loads: Tests whether the article page loads successfully.
|
||||
2. test_page_move_creates_redirect: Tests whether moving the article page
|
||||
under the index page AUTOMATICALLY creates a redirect.
|
||||
3. test_page_move_creates_redirect_fr and test_page_move_creates_redirect_de:
|
||||
Similar to the second test, but for translated pages in French and German.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_superuser("admin-user", "admin@example.com", "password")
|
||||
self.client.force_login(self.user)
|
||||
self.article_page_under_home_page = publication_factory.ArticlePageFactory(
|
||||
parent=self.homepage, title="Home page article"
|
||||
)
|
||||
self.article_index_page = buyersguide_factories.BuyersGuideEditorialContentIndexPageFactory(
|
||||
parent=self.homepage, title="News index"
|
||||
)
|
||||
self.article_page_under_index_page = publication_factory.ArticlePageFactory(
|
||||
parent=self.article_index_page, title="News article"
|
||||
)
|
||||
self.article_page_under_index_page_old_path = self.article_page_under_index_page.url
|
||||
|
||||
# Translate some pages
|
||||
self.en_locale = Locale.objects.get(language_code="en")
|
||||
self.fr_locale = Locale.objects.get(language_code="fr")
|
||||
self.de_locale = Locale.objects.get(language_code="de")
|
||||
|
||||
self.fr_homepage = self.homepage.copy_for_translation(self.fr_locale)
|
||||
self.de_homepage = self.homepage.copy_for_translation(self.de_locale)
|
||||
|
||||
self.fr_article_page_under_home_page = self.article_page_under_home_page.copy_for_translation(self.fr_locale)
|
||||
self.fr_article_index_page = self.article_index_page.copy_for_translation(self.fr_locale)
|
||||
self.fr_article_page_under_index_page = self.article_page_under_index_page.copy_for_translation(self.fr_locale)
|
||||
|
||||
self.fr_article_page_under_index_page_old_path = self.fr_article_page_under_index_page.url
|
||||
|
||||
self.de_article_page_under_home_page = self.article_page_under_home_page.copy_for_translation(self.de_locale)
|
||||
self.de_article_index_page = self.article_index_page.copy_for_translation(self.de_locale)
|
||||
self.de_article_page_under_index_page = self.article_page_under_index_page.copy_for_translation(self.de_locale)
|
||||
|
||||
self.de_article_page_under_index_page_old_path = self.de_article_page_under_index_page.url
|
||||
|
||||
def test_page_loads(self):
|
||||
response = self.client.get(path=self.article_page_under_home_page.get_url())
|
||||
self.assertEqual(response.status_code, http.HTTPStatus.OK)
|
||||
|
||||
def test_page_move_creates_redirect(self):
|
||||
# Check there are no redirects yet
|
||||
self.assertFalse(Redirect.objects.all())
|
||||
# Move the article under the index page out under the home page.
|
||||
response = self.client.post(
|
||||
reverse("wagtailadmin_pages:move_confirm", args=(self.article_page_under_index_page.id, self.homepage.id)),
|
||||
follow=True,
|
||||
)
|
||||
# Check the 'Page moved' message to show it's been moved
|
||||
self.assertContains(response, f"Page '{self.article_page_under_index_page}' moved")
|
||||
self.assertTrue(Redirect.objects.all())
|
||||
redirect = Redirect.objects.first()
|
||||
# check the redirect
|
||||
# +'/' because old path doesn't store trailing slash
|
||||
self.assertEqual(redirect.old_path + "/", self.article_page_under_index_page_old_path)
|
||||
self.assertEqual(redirect.redirect_page_id, self.article_page_under_index_page.id)
|
||||
|
||||
def test_page_move_creates_redirect_fr(self):
|
||||
# Check there are no redirects yet
|
||||
self.assertFalse(Redirect.objects.all())
|
||||
# Move the article under the index page out under the home page.
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"wagtailadmin_pages:move_confirm", args=(self.fr_article_page_under_index_page.id, self.fr_homepage.id)
|
||||
),
|
||||
follow=True,
|
||||
)
|
||||
# Check the 'Page moved' message to show it's been moved
|
||||
self.assertContains(response, f"Page '{self.fr_article_page_under_index_page}' moved")
|
||||
self.assertTrue(Redirect.objects.all())
|
||||
redirect = Redirect.objects.first()
|
||||
# check the redirect
|
||||
# +'/' because old path doesn't store trailing slash
|
||||
self.assertEqual(redirect.old_path + "/", self.fr_article_page_under_index_page_old_path)
|
||||
self.assertEqual(redirect.redirect_page_id, self.fr_article_page_under_index_page.id)
|
||||
|
||||
def test_page_move_creates_redirect_de(self):
|
||||
# Check there are no redirects yet
|
||||
self.assertFalse(Redirect.objects.all())
|
||||
# Move the article under the index page out under the home page.
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"wagtailadmin_pages:move_confirm", args=(self.de_article_page_under_index_page.id, self.de_homepage.id)
|
||||
),
|
||||
follow=True,
|
||||
)
|
||||
# Check the 'Page moved' message to show it's been moved
|
||||
self.assertContains(response, f"Page '{self.de_article_page_under_index_page}' moved")
|
||||
self.assertTrue(Redirect.objects.all())
|
||||
redirect = Redirect.objects.first()
|
||||
# check the redirect
|
||||
# +'/' because old path doesn't store trailing slash
|
||||
self.assertEqual(redirect.old_path + "/", self.de_article_page_under_index_page_old_path)
|
||||
self.assertEqual(redirect.redirect_page_id, self.de_article_page_under_index_page.id)
|
|
@ -1,3 +1,5 @@
|
|||
from networkapi.wagtailpages.factory.libraries import research_hub
|
||||
|
||||
from . import (
|
||||
bannered_campaign_page,
|
||||
blog,
|
||||
|
@ -17,7 +19,6 @@ from . import (
|
|||
participate_page_featured_highlights,
|
||||
profiles,
|
||||
publication,
|
||||
research_hub,
|
||||
styleguide,
|
||||
youtube_regrets_page,
|
||||
)
|
||||
|
|
|
@ -513,7 +513,7 @@ def generate(seed):
|
|||
|
||||
# Adding related articles to Categories
|
||||
for product_category in pagemodels.BuyersGuideProductCategory.objects.all():
|
||||
for index, article in enumerate(get_random_objects(pagemodels.BuyersGuideArticlePage, max_count=6)):
|
||||
for index, article in enumerate(get_random_objects(pagemodels.BuyersGuideArticlePage, exact_count=6)):
|
||||
BuyersGuideProductCategoryArticlePageRelationFactory(
|
||||
category=product_category,
|
||||
article=article,
|
||||
|
|
|
@ -2,6 +2,7 @@ from factory import SubFactory, Trait
|
|||
from wagtail.models import Page as WagtailPage
|
||||
|
||||
from networkapi.utility.faker.helpers import get_homepage, reseed
|
||||
from networkapi.wagtailpages.donation_modal import DonationModals
|
||||
from networkapi.wagtailpages.models import CampaignIndexPage, CampaignPage
|
||||
|
||||
from .abstract import CMSPageFactory
|
||||
|
@ -21,6 +22,11 @@ class CampaignPageFactory(CMSPageFactory):
|
|||
|
||||
class Params:
|
||||
no_cta = Trait(cta=None)
|
||||
cta_show_all_fields = Trait(
|
||||
cta=SubFactory(
|
||||
PetitionFactory, requires_country_code=True, requires_postal_code=True, comment_requirements="required"
|
||||
)
|
||||
)
|
||||
|
||||
cta = SubFactory(PetitionFactory)
|
||||
|
||||
|
@ -53,7 +59,15 @@ def generate(seed):
|
|||
print("single-page CampaignPage already exists")
|
||||
except CampaignPage.DoesNotExist:
|
||||
print("Generating single-page CampaignPage")
|
||||
CampaignPageFactory.create(parent=campaign_index_page, title="single-page")
|
||||
# Most Campaign Pages on prod use wide content layout,
|
||||
# Setting narrowed_page_content to False to make it easier to test the real use case
|
||||
CampaignPageFactory.create(
|
||||
parent=campaign_index_page,
|
||||
title="single-page",
|
||||
narrowed_page_content=False,
|
||||
donation_modals=[DonationModals.objects.first()],
|
||||
cta_show_all_fields=True,
|
||||
)
|
||||
|
||||
reseed(seed)
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import factory
|
||||
from factory.django import DjangoModelFactory, ImageField
|
||||
from wagtail.images import get_image_model
|
||||
from wagtail.models import Collection
|
||||
|
@ -6,8 +7,16 @@ from wagtail.models import Collection
|
|||
# always generates images in the Root collection:
|
||||
|
||||
|
||||
class CollectionFactory(DjangoModelFactory):
|
||||
class Meta:
|
||||
model = Collection
|
||||
django_get_or_create = ("name",)
|
||||
|
||||
name = "Root"
|
||||
|
||||
|
||||
class CollectionMemberFactory(DjangoModelFactory):
|
||||
collection = Collection.objects.get(name="Root")
|
||||
collection = factory.SubFactory(CollectionFactory)
|
||||
|
||||
|
||||
class ImageFactory(CollectionMemberFactory):
|
||||
|
|
|
@ -1,21 +1,39 @@
|
|||
from networkapi.utility.faker import helpers as faker_helpers
|
||||
from networkapi.wagtailpages import models as wagtailpage_models
|
||||
from networkapi.wagtailpages.factory.research_hub import (
|
||||
from networkapi.wagtailpages.factory.libraries.research_hub import (
|
||||
author_index as author_index_factory,
|
||||
)
|
||||
from networkapi.wagtailpages.factory.research_hub import (
|
||||
from networkapi.wagtailpages.factory.libraries.research_hub import (
|
||||
detail_page as detail_page_factory,
|
||||
)
|
||||
from networkapi.wagtailpages.factory.research_hub import (
|
||||
from networkapi.wagtailpages.factory.libraries.research_hub import (
|
||||
landing_page as landing_page_factory,
|
||||
)
|
||||
from networkapi.wagtailpages.factory.research_hub import (
|
||||
from networkapi.wagtailpages.factory.libraries.research_hub import (
|
||||
library_page as library_page_factory,
|
||||
)
|
||||
from networkapi.wagtailpages.factory.research_hub import relations as relations_factory
|
||||
from networkapi.wagtailpages.factory.research_hub import (
|
||||
from networkapi.wagtailpages.factory.libraries.research_hub import (
|
||||
relations as relations_factory,
|
||||
)
|
||||
from networkapi.wagtailpages.factory.libraries.research_hub import (
|
||||
taxonomies as taxonomies_factory,
|
||||
)
|
||||
from networkapi.wagtailpages.pagemodels.profiles import Profile
|
||||
|
||||
|
||||
def create_detail_page_for_visual_regression_tests(seed, research_library_page):
|
||||
faker_helpers.reseed(seed)
|
||||
|
||||
percy_author_profile = Profile.objects.get(name="Percy Profile")
|
||||
|
||||
percy_research_detail_page = detail_page_factory.ResearchDetailPageFactory.create(
|
||||
parent=research_library_page, title="Fixed Title Research Detail Page"
|
||||
)
|
||||
|
||||
relations_factory.ResearchAuthorRelationFactory.create(
|
||||
research_detail_page=percy_research_detail_page,
|
||||
author_profile=percy_author_profile,
|
||||
)
|
||||
|
||||
|
||||
def generate(seed):
|
||||
|
@ -41,6 +59,8 @@ def generate(seed):
|
|||
parent=research_landing_page
|
||||
)
|
||||
|
||||
create_detail_page_for_visual_regression_tests(seed, research_library_page)
|
||||
|
||||
for _ in range(4):
|
||||
taxonomies_factory.ResearchRegionFactory.create()
|
||||
taxonomies_factory.ResearchTopicFactory.create()
|
|
@ -16,7 +16,7 @@ class ResearchDetailPageFactory(wagtail_factories.PageFactory):
|
|||
title = factory.Faker("text", max_nb_chars=50)
|
||||
cover_image = factory.SubFactory(image_factory.ImageFactory)
|
||||
research_links = factory.RelatedFactoryList(
|
||||
factory="networkapi.wagtailpages.factory.research_hub.detail_page.ResearchDetailLinkFactory",
|
||||
factory="networkapi.wagtailpages.factory.libraries.research_hub.detail_page.ResearchDetailLinkFactory",
|
||||
factory_related_name="research_detail_page",
|
||||
size=lambda: random.randint(1, 2),
|
||||
with_url=True,
|
||||
|
@ -38,14 +38,15 @@ class ResearchDetailPageFactory(wagtail_factories.PageFactory):
|
|||
return "; ".join(names)
|
||||
|
||||
research_authors = factory.RelatedFactoryList(
|
||||
factory="networkapi.wagtailpages.factory.research_hub.relations.ResearchAuthorRelationFactory",
|
||||
factory="networkapi.wagtailpages.factory.libraries.research_hub.relations.ResearchAuthorRelationFactory",
|
||||
factory_related_name="research_detail_page",
|
||||
size=1,
|
||||
)
|
||||
|
||||
related_topics = factory.RelatedFactoryList(
|
||||
factory=(
|
||||
"networkapi.wagtailpages.factory.research_hub.relations.ResearchDetailPageResearchTopicRelationFactory"
|
||||
"networkapi.wagtailpages.factory.libraries.research_hub"
|
||||
".relations.ResearchDetailPageResearchTopicRelationFactory"
|
||||
),
|
||||
factory_related_name="research_detail_page",
|
||||
size=1,
|
||||
|
@ -53,7 +54,8 @@ class ResearchDetailPageFactory(wagtail_factories.PageFactory):
|
|||
|
||||
related_regions = factory.RelatedFactoryList(
|
||||
factory=(
|
||||
"networkapi.wagtailpages.factory.research_hub.relations.ResearchDetailPageResearchRegionRelationFactory"
|
||||
"networkapi.wagtailpages.factory.libraries.research_hub"
|
||||
".relations.ResearchDetailPageResearchRegionRelationFactory"
|
||||
),
|
||||
factory_related_name="research_detail_page",
|
||||
size=1,
|
|
@ -2,13 +2,13 @@ import factory
|
|||
|
||||
from networkapi.wagtailpages import models as wagtailpage_models
|
||||
from networkapi.wagtailpages.factory import profiles as profiles_factory
|
||||
from networkapi.wagtailpages.factory.research_hub import (
|
||||
from networkapi.wagtailpages.factory.libraries.research_hub import (
|
||||
detail_page as detail_page_factory,
|
||||
)
|
||||
from networkapi.wagtailpages.factory.research_hub import (
|
||||
from networkapi.wagtailpages.factory.libraries.research_hub import (
|
||||
landing_page as landing_page_factory,
|
||||
)
|
||||
from networkapi.wagtailpages.factory.research_hub import (
|
||||
from networkapi.wagtailpages.factory.libraries.research_hub import (
|
||||
taxonomies as taxonomies_factory,
|
||||
)
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import factory
|
||||
from django.utils import text as text_utils
|
||||
|
||||
from networkapi.wagtailpages import models as wagtailpage_models
|
||||
|
||||
|
@ -9,6 +10,10 @@ class ResearchRegionFactory(factory.django.DjangoModelFactory):
|
|||
|
||||
name = factory.Faker("text", max_nb_chars=25)
|
||||
|
||||
@factory.post_generation
|
||||
def set_slug(obj, created, extracted, **kwargs):
|
||||
obj.slug = text_utils.slugify(obj.name)
|
||||
|
||||
|
||||
class ResearchTopicFactory(factory.django.DjangoModelFactory):
|
||||
class Meta:
|
||||
|
@ -16,3 +21,7 @@ class ResearchTopicFactory(factory.django.DjangoModelFactory):
|
|||
|
||||
name = factory.Faker("text", max_nb_chars=25)
|
||||
description = factory.Faker("paragraph")
|
||||
|
||||
@factory.post_generation
|
||||
def set_slug(obj, created, extracted, **kwargs):
|
||||
obj.slug = text_utils.slugify(obj.name)
|
|
@ -22,5 +22,11 @@ class ProfileFactory(DjangoModelFactory):
|
|||
def generate(seed):
|
||||
reseed(seed)
|
||||
|
||||
print("Generating profiles")
|
||||
print("Generating a profile that can be used for percy testing.")
|
||||
ProfileFactory(
|
||||
name="Percy Profile",
|
||||
tagline="A profile made for visual regression testing.",
|
||||
)
|
||||
|
||||
print("Generating other profiles")
|
||||
generate_fake_data(ProfileFactory, NUM_PROFILES)
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 3.2.18 on 2023-05-05 02:44
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("wagtailpages", "0081_alter_body_field_on_pages"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="buyersguidecalltoaction",
|
||||
name="link_target_url",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
max_length=255,
|
||||
validators=[
|
||||
django.core.validators.RegexValidator(
|
||||
message="Please enter a valid URL (starting with http:// or https://), or a valid query string starting with ? (Ex: ?form=donate).",
|
||||
regex="^(https?://[\\w.-]+(/\\S*)?)?(\\?[\\w-]+(=[\\w-]*)?(&[\\w-]+(=[\\w-]*)?)*)?$",
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,40 @@
|
|||
# Generated by Django 3.2.18 on 2023-05-17 11:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("wagtailcore", "0078_referenceindex"),
|
||||
("wagtailpages", "0082_alter_buyersguidecalltoaction_link_target_url"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="researchregion",
|
||||
name="slug",
|
||||
field=models.SlugField(
|
||||
help_text="The slug is auto-generated from the name, but can be customized if needed. It needs to be unique per locale.",
|
||||
max_length=100,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="researchtopic",
|
||||
name="slug",
|
||||
field=models.SlugField(
|
||||
help_text="The slug is auto-generated from the name, but can be customized if needed. It needs to be unique per locale.",
|
||||
max_length=100,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="researchregion",
|
||||
unique_together={("translation_key", "locale"), ("locale", "slug")},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="researchtopic",
|
||||
unique_together={("translation_key", "locale"), ("locale", "slug")},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,43 @@
|
|||
from django.db import migrations
|
||||
from django.utils.text import slugify
|
||||
|
||||
|
||||
def set_default_slug(apps, schema_editor):
|
||||
ResearchRegion = apps.get_model("wagtailpages", "ResearchRegion")
|
||||
ResearchTopic = apps.get_model("wagtailpages", "ResearchTopic")
|
||||
|
||||
for region in ResearchRegion.objects.all():
|
||||
slug = slugify(region.name)
|
||||
unique_slug = slug
|
||||
counter = 1
|
||||
|
||||
while ResearchRegion.objects.filter(slug=unique_slug).exists():
|
||||
unique_slug = f"{slug}-{counter}"
|
||||
counter += 1
|
||||
|
||||
region.slug = unique_slug
|
||||
region.save()
|
||||
|
||||
for topic in ResearchTopic.objects.all():
|
||||
slug = slugify(topic.name)
|
||||
unique_slug = slug
|
||||
counter = 1
|
||||
|
||||
while ResearchTopic.objects.filter(slug=unique_slug).exists():
|
||||
unique_slug = f"{slug}-{counter}"
|
||||
counter += 1
|
||||
|
||||
topic.slug = unique_slug
|
||||
topic.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("wagtailcore", "0078_referenceindex"),
|
||||
("wagtailpages", "0083_add_slug_to_base_taxonomy"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(set_default_slug, reverse_code=migrations.RunPython.noop),
|
||||
]
|
|
@ -0,0 +1,38 @@
|
|||
# Generated by Django 3.2.18 on 2023-05-17 12:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
"""
|
||||
After the previous migration has run, we can now set the slug field
|
||||
as non-nullable since it has been populated with data.
|
||||
"""
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("wagtailpages", "0084_set_slugs_for_existing_taxonomy"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="researchregion",
|
||||
name="slug",
|
||||
field=models.SlugField(
|
||||
default="",
|
||||
help_text="The slug is auto-generated from the name, but can be customized if needed. It needs to be unique per locale.",
|
||||
max_length=100,
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="researchtopic",
|
||||
name="slug",
|
||||
field=models.SlugField(
|
||||
default="",
|
||||
help_text="The slug is auto-generated from the name, but can be customized if needed. It needs to be unique per locale.",
|
||||
max_length=100,
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
|
@ -11,6 +11,7 @@ from .pagemodels.base import (
|
|||
ParticipateHighlights2,
|
||||
ParticipatePage2,
|
||||
PartnerLogos,
|
||||
PrimaryPage,
|
||||
Styleguide,
|
||||
)
|
||||
from .pagemodels.blog.blog import BlogAuthors, BlogPage
|
||||
|
@ -63,27 +64,29 @@ from .pagemodels.campaigns import (
|
|||
from .pagemodels.dear_internet import DearInternetPage
|
||||
from .pagemodels.feature_flags.feature_flags import FeatureFlags
|
||||
from .pagemodels.index import IndexPage
|
||||
from .pagemodels.mixin.foundation_banner_inheritance import (
|
||||
FoundationBannerInheritanceMixin,
|
||||
from .pagemodels.libraries.research_hub.authors_index import ResearchAuthorsIndexPage
|
||||
from .pagemodels.libraries.research_hub.detail_page import (
|
||||
ResearchDetailLink,
|
||||
ResearchDetailPage,
|
||||
)
|
||||
from .pagemodels.modular import MiniSiteNameSpace, ModularPage
|
||||
from .pagemodels.primary import PrimaryPage
|
||||
from .pagemodels.profiles import Profile
|
||||
from .pagemodels.publications.article import ArticlePage
|
||||
from .pagemodels.publications.publication import PublicationPage
|
||||
from .pagemodels.pulse import PulseFilter
|
||||
from .pagemodels.redirect import RedirectingPage
|
||||
from .pagemodels.research_hub.authors_index import ResearchAuthorsIndexPage
|
||||
from .pagemodels.research_hub.detail_page import ResearchDetailLink, ResearchDetailPage
|
||||
from .pagemodels.research_hub.landing_page import ResearchLandingPage
|
||||
from .pagemodels.research_hub.library_page import ResearchLibraryPage
|
||||
from .pagemodels.research_hub.relations import (
|
||||
from .pagemodels.libraries.research_hub.landing_page import ResearchLandingPage
|
||||
from .pagemodels.libraries.research_hub.library_page import ResearchLibraryPage
|
||||
from .pagemodels.libraries.research_hub.relations import (
|
||||
ResearchAuthorRelation,
|
||||
ResearchDetailPageResearchRegionRelation,
|
||||
ResearchDetailPageResearchTopicRelation,
|
||||
ResearchLandingPageFeaturedResearchTopicRelation,
|
||||
)
|
||||
from .pagemodels.research_hub.taxonomies import ResearchRegion, ResearchTopic
|
||||
from .pagemodels.libraries.research_hub.taxonomies import ResearchRegion, ResearchTopic
|
||||
from .pagemodels.mixin.foundation_banner_inheritance import (
|
||||
FoundationBannerInheritanceMixin,
|
||||
)
|
||||
from .pagemodels.modular import MiniSiteNameSpace, ModularPage
|
||||
from .pagemodels.profiles import Profile
|
||||
from .pagemodels.publications.article import ArticlePage
|
||||
from .pagemodels.publications.publication import PublicationPage
|
||||
from .pagemodels.pulse import PulseFilter
|
||||
from .pagemodels.redirect import RedirectingPage
|
||||
from .pagemodels.youtube import (
|
||||
YoutubeRegrets2021Page,
|
||||
YoutubeRegrets2022Page,
|
||||
|
|
|
@ -2,7 +2,7 @@ from django.conf import settings
|
|||
from django.db import models
|
||||
from modelcluster.fields import ParentalKey
|
||||
from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel
|
||||
from wagtail.fields import RichTextField
|
||||
from wagtail.fields import RichTextField, StreamField
|
||||
from wagtail.images import get_image_model_string
|
||||
from wagtail.models import Orderable as WagtailOrderable
|
||||
from wagtail.models import Page, TranslatableMixin
|
||||
|
@ -11,10 +11,112 @@ from wagtail_localize.fields import SynchronizedField, TranslatableField
|
|||
|
||||
# TODO: https://github.com/mozilla/foundation.mozilla.org/issues/2362
|
||||
from ..donation_modal import DonationModals # noqa: F401
|
||||
from ..utils import TitleWidget
|
||||
from ..utils import TitleWidget, get_page_tree_information
|
||||
from .customblocks.base_fields import base_fields
|
||||
from .customblocks.base_rich_text_options import base_rich_text_options
|
||||
from .mixin.foundation_banner_inheritance import FoundationBannerInheritanceMixin
|
||||
from .mixin.foundation_metadata import FoundationMetadataPageMixin
|
||||
from .primary import PrimaryPage
|
||||
from .mixin.foundation_navigation import FoundationNavigationPageMixin
|
||||
|
||||
|
||||
class BasePage(FoundationMetadataPageMixin, FoundationNavigationPageMixin, Page):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class PrimaryPage(FoundationBannerInheritanceMixin, BasePage): # type: ignore
|
||||
"""
|
||||
Basically a straight copy of modular page, but with
|
||||
restrictions on what can live 'under it'.
|
||||
|
||||
Ideally this is just PrimaryPage(ModularPage) but
|
||||
setting that up as a migration seems to be causing
|
||||
problems.
|
||||
"""
|
||||
|
||||
header = models.CharField(max_length=250, blank=True)
|
||||
|
||||
banner = models.ForeignKey(
|
||||
"wagtailimages.Image",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="primary_banner",
|
||||
verbose_name="Hero Image",
|
||||
help_text="Choose an image that's bigger than 4032px x 1152px with aspect ratio 3.5:1",
|
||||
)
|
||||
|
||||
intro = models.CharField(
|
||||
max_length=350,
|
||||
blank=True,
|
||||
help_text="Intro paragraph to show in hero cutout box",
|
||||
)
|
||||
|
||||
narrowed_page_content = models.BooleanField(
|
||||
default=False,
|
||||
help_text="For text-heavy pages, turn this on to reduce the overall width of the content on the page.",
|
||||
)
|
||||
|
||||
zen_nav = models.BooleanField(
|
||||
default=False,
|
||||
help_text="For secondary nav pages, use this to collapse the primary nav under a toggle hamburger.",
|
||||
)
|
||||
|
||||
body = StreamField(base_fields, use_json_field=True)
|
||||
|
||||
settings_panels = Page.settings_panels + [
|
||||
MultiFieldPanel(
|
||||
[
|
||||
FieldPanel("narrowed_page_content"),
|
||||
],
|
||||
classname="collapsible",
|
||||
),
|
||||
MultiFieldPanel(
|
||||
[
|
||||
FieldPanel("zen_nav"),
|
||||
],
|
||||
classname="collapsible",
|
||||
),
|
||||
]
|
||||
|
||||
content_panels = Page.content_panels + [
|
||||
FieldPanel("header"),
|
||||
FieldPanel("banner"),
|
||||
FieldPanel("intro"),
|
||||
FieldPanel("body"),
|
||||
]
|
||||
|
||||
translatable_fields = [
|
||||
# Promote tab fields
|
||||
SynchronizedField("slug"),
|
||||
TranslatableField("seo_title"),
|
||||
SynchronizedField("show_in_menus"),
|
||||
TranslatableField("search_description"),
|
||||
SynchronizedField("search_image"),
|
||||
# Content tab fields
|
||||
TranslatableField("title"),
|
||||
TranslatableField("header"),
|
||||
SynchronizedField("banner"),
|
||||
TranslatableField("intro"),
|
||||
TranslatableField("body"),
|
||||
SynchronizedField("narrowed_page_content"),
|
||||
SynchronizedField("zen_nav"),
|
||||
]
|
||||
|
||||
subpage_types = [
|
||||
"PrimaryPage",
|
||||
"RedirectingPage",
|
||||
"BanneredCampaignPage",
|
||||
"OpportunityPage",
|
||||
"ArticlePage",
|
||||
]
|
||||
|
||||
show_in_menus_default = True
|
||||
|
||||
def get_context(self, request):
|
||||
context = super().get_context(request)
|
||||
context = get_page_tree_information(self, context)
|
||||
return context
|
||||
|
||||
|
||||
class InitiativeSection(TranslatableMixin, models.Model):
|
||||
|
|
|
@ -23,14 +23,10 @@ from wagtail_localize.fields import SynchronizedField, TranslatableField
|
|||
from networkapi.wagtailpages.forms import BlogPageForm
|
||||
from networkapi.wagtailpages.pagemodels.profiles import Profile
|
||||
|
||||
from ...utils import (
|
||||
TitleWidget,
|
||||
get_content_related_by_tag,
|
||||
set_main_site_nav_information,
|
||||
)
|
||||
from ...utils import TitleWidget, get_content_related_by_tag
|
||||
from .. import customblocks
|
||||
from ..base import BasePage
|
||||
from ..customblocks.full_content_rich_text_options import full_content_rich_text_options
|
||||
from ..mixin.foundation_metadata import FoundationMetadataPageMixin
|
||||
from .blog_index import BlogIndexPage
|
||||
from .blog_topic import BlogPageTopic
|
||||
|
||||
|
@ -110,7 +106,7 @@ class RelatedBlogPosts(Orderable):
|
|||
ordering = ["sort_order"]
|
||||
|
||||
|
||||
class BlogPage(FoundationMetadataPageMixin, Page):
|
||||
class BlogPage(BasePage):
|
||||
# Custom base form for additional validation
|
||||
base_form_class = BlogPageForm
|
||||
|
||||
|
@ -279,7 +275,7 @@ class BlogPage(FoundationMetadataPageMixin, Page):
|
|||
if blog_page:
|
||||
context["blog_index"] = blog_page
|
||||
|
||||
return set_main_site_nav_information(self, context, "Homepage")
|
||||
return context
|
||||
|
||||
def get_missing_related_posts(self):
|
||||
"""
|
||||
|
|
|
@ -20,6 +20,7 @@ from networkapi.wagtailpages.forms import BlogIndexPageForm
|
|||
from networkapi.wagtailpages.pagemodels import customblocks
|
||||
from networkapi.wagtailpages.pagemodels.profiles import Profile
|
||||
from networkapi.wagtailpages.utils import (
|
||||
get_blog_authors,
|
||||
get_default_locale,
|
||||
get_locale_from_request,
|
||||
localize_queryset,
|
||||
|
@ -382,7 +383,7 @@ class BlogIndexPage(IndexPage):
|
|||
Profiles used as blog authors.
|
||||
"""
|
||||
author_profiles = Profile.objects.all()
|
||||
author_profiles = author_profiles.filter_blog_authors()
|
||||
author_profiles = get_blog_authors(author_profiles)
|
||||
author_profiles = localize_queryset(author_profiles)
|
||||
|
||||
return self.render(
|
||||
|
|
|
@ -11,20 +11,20 @@ from wagtail_localize.fields import SynchronizedField, TranslatableField
|
|||
|
||||
from networkapi.utility import orderables
|
||||
from networkapi.wagtailpages.pagemodels import customblocks
|
||||
from networkapi.wagtailpages.pagemodels.base import BasePage
|
||||
from networkapi.wagtailpages.pagemodels.buyersguide.forms import (
|
||||
BuyersGuideArticlePageForm,
|
||||
)
|
||||
from networkapi.wagtailpages.pagemodels.buyersguide.utils import (
|
||||
get_categories_for_locale,
|
||||
)
|
||||
from networkapi.wagtailpages.pagemodels.mixin import foundation_metadata
|
||||
from networkapi.wagtailpages.utils import get_language_from_request
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from networkapi.wagtailpages.models import BuyersGuideContentCategory, Profile
|
||||
|
||||
|
||||
class BuyersGuideArticlePage(foundation_metadata.FoundationMetadataPageMixin, wagtail_models.Page):
|
||||
class BuyersGuideArticlePage(BasePage):
|
||||
parent_page_types = ["wagtailpages.BuyersGuideEditorialContentIndexPage"]
|
||||
subpage_types: list = []
|
||||
template = "pages/buyersguide/article_page.html"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.forms.utils import ErrorList
|
||||
from wagtail.admin.panels import FieldPanel, MultiFieldPanel
|
||||
|
@ -13,6 +14,9 @@ from networkapi.wagtailpages.pagemodels.customblocks.base_rich_text_options impo
|
|||
)
|
||||
from networkapi.wagtailpages.pagemodels.mixin.snippets import LocalizedSnippet
|
||||
|
||||
# Validates whether a string is either a valid URL, a query string (?param=test), or both.
|
||||
url_or_query_regex = r"^(https?://[\w.-]+(/\S*)?)?(\?[\w-]+(=[\w-]*)?(&[\w-]+(=[\w-]*)?)*)?$"
|
||||
|
||||
|
||||
@register_snippet
|
||||
class BuyersGuideCallToAction(index.Indexed, TranslatableMixin, LocalizedSnippet, models.Model):
|
||||
|
@ -32,7 +36,19 @@ class BuyersGuideCallToAction(index.Indexed, TranslatableMixin, LocalizedSnippet
|
|||
title = models.CharField(max_length=200)
|
||||
content = RichTextField(features=base_rich_text_options, blank=True)
|
||||
link_label = models.CharField(max_length=2048, blank=True)
|
||||
link_target_url = models.URLField(blank=True)
|
||||
link_target_url = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
validators=[
|
||||
RegexValidator(
|
||||
regex=url_or_query_regex,
|
||||
message=(
|
||||
"Please enter a valid URL (starting with http:// or https://), "
|
||||
"or a valid query string starting with ? (Ex: ?form=donate)."
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
link_target_page = models.ForeignKey(
|
||||
Page,
|
||||
null=True,
|
||||
|
|
|
@ -9,12 +9,10 @@ from wagtail.models import Orderable, Page, TranslatableMixin
|
|||
from wagtail_localize.fields import SynchronizedField, TranslatableField
|
||||
|
||||
from networkapi.wagtailpages.pagemodels import customblocks
|
||||
from networkapi.wagtailpages.pagemodels.base import BasePage
|
||||
from networkapi.wagtailpages.pagemodels.buyersguide.utils import (
|
||||
get_categories_for_locale,
|
||||
)
|
||||
from networkapi.wagtailpages.pagemodels.mixin.foundation_metadata import (
|
||||
FoundationMetadataPageMixin,
|
||||
)
|
||||
from networkapi.wagtailpages.utils import get_language_from_request
|
||||
|
||||
from ..customblocks.full_content_rich_text_options import full_content_rich_text_options
|
||||
|
@ -41,7 +39,7 @@ class BuyersGuideCampaignPageDonationModalRelation(TranslatableMixin, Orderable)
|
|||
pass
|
||||
|
||||
|
||||
class BuyersGuideCampaignPage(FoundationMetadataPageMixin, Page):
|
||||
class BuyersGuideCampaignPage(BasePage):
|
||||
parent_page_types = ["BuyersGuideEditorialContentIndexPage"]
|
||||
subpage_types: list = []
|
||||
template = "pages/buyersguide/campaign_page.html"
|
||||
|
|
|
@ -11,11 +11,11 @@ from wagtail.models import Orderable, TranslatableMixin
|
|||
from wagtail_localize.fields import SynchronizedField, TranslatableField
|
||||
|
||||
from networkapi.utility import orderables
|
||||
from networkapi.wagtailpages.pagemodels.base import BasePage
|
||||
from networkapi.wagtailpages.pagemodels.buyersguide.utils import (
|
||||
get_buyersguide_featured_cta,
|
||||
get_categories_for_locale,
|
||||
)
|
||||
from networkapi.wagtailpages.pagemodels.mixin import foundation_metadata
|
||||
from networkapi.wagtailpages.utils import get_language_from_request
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -25,9 +25,8 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class BuyersGuideEditorialContentIndexPage(
|
||||
foundation_metadata.FoundationMetadataPageMixin,
|
||||
routable_models.RoutablePageMixin,
|
||||
wagtail_models.Page,
|
||||
BasePage,
|
||||
):
|
||||
parent_page_types = ["wagtailpages.BuyersGuidePage"]
|
||||
subpage_types = [
|
||||
|
|
|
@ -19,17 +19,15 @@ from wagtail.admin.panels import (
|
|||
PageChooserPanel,
|
||||
)
|
||||
from wagtail.contrib.routable_page.models import RoutablePageMixin, route
|
||||
from wagtail.models import Locale, Orderable, Page, TranslatableMixin
|
||||
from wagtail.models import Locale, Orderable, TranslatableMixin
|
||||
from wagtail_localize.fields import SynchronizedField, TranslatableField
|
||||
|
||||
from networkapi.utility import orderables
|
||||
from networkapi.wagtailpages.pagemodels.base import BasePage
|
||||
from networkapi.wagtailpages.pagemodels.buyersguide.utils import (
|
||||
get_categories_for_locale,
|
||||
sort_average,
|
||||
)
|
||||
from networkapi.wagtailpages.pagemodels.mixin.foundation_metadata import (
|
||||
FoundationMetadataPageMixin,
|
||||
)
|
||||
from networkapi.wagtailpages.templatetags.localization import relocalize_url
|
||||
from networkapi.wagtailpages.utils import (
|
||||
get_default_locale,
|
||||
|
@ -45,7 +43,7 @@ if TYPE_CHECKING:
|
|||
)
|
||||
|
||||
|
||||
class BuyersGuidePage(RoutablePageMixin, FoundationMetadataPageMixin, Page):
|
||||
class BuyersGuidePage(RoutablePageMixin, BasePage):
|
||||
"""
|
||||
Note: We'll likely be converting the "about" pages to Wagtail Pages.
|
||||
When that happens, we should remove the RoutablePageMixin and @routes
|
||||
|
@ -361,6 +359,7 @@ class BuyersGuidePage(RoutablePageMixin, FoundationMetadataPageMixin, Page):
|
|||
return sitemap
|
||||
|
||||
def get_context(self, request, *args, **kwargs):
|
||||
bypass_products = kwargs.pop("bypass_products", False)
|
||||
context = super().get_context(request, *args, **kwargs)
|
||||
language_code = get_language_from_request(request)
|
||||
|
||||
|
@ -371,7 +370,7 @@ class BuyersGuidePage(RoutablePageMixin, FoundationMetadataPageMixin, Page):
|
|||
exclude_cat_ids = [excats.category.id for excats in self.excluded_categories.all()]
|
||||
|
||||
ProductPage = apps.get_model(app_label="wagtailpages", model_name="ProductPage")
|
||||
if not kwargs.get("bypass_products", False) and products is None:
|
||||
if not bypass_products and products is None:
|
||||
products = get_product_subset(
|
||||
self.cutoff_date,
|
||||
authenticated,
|
||||
|
|
|
@ -29,6 +29,7 @@ from wagtail_localize.fields import SynchronizedField, TranslatableField
|
|||
|
||||
from networkapi.utility import orderables
|
||||
from networkapi.wagtailpages.fields import ExtendedYesNoField
|
||||
from networkapi.wagtailpages.pagemodels.base import BasePage
|
||||
from networkapi.wagtailpages.pagemodels.buyersguide.forms import (
|
||||
BuyersGuideProductCategoryForm,
|
||||
)
|
||||
|
@ -39,9 +40,6 @@ from networkapi.wagtailpages.pagemodels.buyersguide.utils import (
|
|||
from networkapi.wagtailpages.pagemodels.customblocks.base_rich_text_options import (
|
||||
base_rich_text_options,
|
||||
)
|
||||
from networkapi.wagtailpages.pagemodels.mixin.foundation_metadata import (
|
||||
FoundationMetadataPageMixin,
|
||||
)
|
||||
from networkapi.wagtailpages.pagemodels.mixin.snippets import LocalizedSnippet
|
||||
from networkapi.wagtailpages.utils import (
|
||||
TitleWidget,
|
||||
|
@ -425,7 +423,7 @@ class ProductUpdates(TranslatableMixin, Orderable):
|
|||
ordering = ["sort_order"]
|
||||
|
||||
|
||||
class ProductPage(FoundationMetadataPageMixin, Page):
|
||||
class ProductPage(BasePage):
|
||||
"""
|
||||
ProductPage is the superclass that GeneralProductPages inherits from.
|
||||
|
||||
|
@ -745,7 +743,7 @@ class ProductPage(FoundationMetadataPageMixin, Page):
|
|||
"privacy_policy_links",
|
||||
label="link",
|
||||
min_num=1,
|
||||
max_num=3,
|
||||
max_num=25,
|
||||
),
|
||||
],
|
||||
heading="Privacy policy links",
|
||||
|
|
|
@ -11,9 +11,9 @@ from wagtail.snippets.models import register_snippet
|
|||
from wagtail_localize.fields import SynchronizedField, TranslatableField
|
||||
|
||||
from ..utils import get_content_related_by_tag, get_page_tree_information
|
||||
from .base import PrimaryPage
|
||||
from .mixin.foundation_metadata import FoundationMetadataPageMixin
|
||||
from .modular import MiniSiteNameSpace
|
||||
from .primary import PrimaryPage
|
||||
|
||||
|
||||
@register_snippet
|
||||
|
|
|
@ -3,6 +3,7 @@ from wagtail import blocks
|
|||
from ..customblocks.base_rich_text_options import base_rich_text_options
|
||||
from .datawrapper_block import DatawrapperBlock
|
||||
from .image_block import ImageBlock
|
||||
from .video_block import VideoBlock
|
||||
|
||||
accordion_rich_text = blocks.RichTextBlock(features=base_rich_text_options + ["ul", "ol", "document-link"], blank=True)
|
||||
|
||||
|
@ -14,6 +15,7 @@ class AccordionItem(blocks.StructBlock):
|
|||
("rich_text", accordion_rich_text),
|
||||
("datawrapper", DatawrapperBlock()),
|
||||
("image", ImageBlock()),
|
||||
("video", VideoBlock()),
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
@ -5,13 +5,12 @@ from wagtail.fields import StreamField
|
|||
from wagtail.models import Page
|
||||
from wagtail_localize.fields import SynchronizedField, TranslatableField
|
||||
|
||||
from ..utils import set_main_site_nav_information
|
||||
from . import customblocks
|
||||
from .base import BasePage
|
||||
from .customblocks.base_rich_text_options import base_rich_text_options
|
||||
from .mixin.foundation_metadata import FoundationMetadataPageMixin
|
||||
|
||||
|
||||
class DearInternetPage(FoundationMetadataPageMixin, Page):
|
||||
class DearInternetPage(BasePage):
|
||||
intro_texts = StreamField(
|
||||
[("intro_text", blocks.RichTextBlock(features=base_rich_text_options))], use_json_field=True
|
||||
)
|
||||
|
@ -71,8 +70,4 @@ class DearInternetPage(FoundationMetadataPageMixin, Page):
|
|||
|
||||
zen_nav = True
|
||||
|
||||
def get_context(self, request):
|
||||
context = super().get_context(request)
|
||||
return set_main_site_nav_information(self, context, "Homepage")
|
||||
|
||||
template = "wagtailpages/pages/dear_internet_page.html"
|
||||
|
|
|
@ -16,13 +16,12 @@ from networkapi.wagtailpages.utils import (
|
|||
get_default_locale,
|
||||
get_locale_from_request,
|
||||
get_page_tree_information,
|
||||
set_main_site_nav_information,
|
||||
)
|
||||
|
||||
from .mixin.foundation_metadata import FoundationMetadataPageMixin
|
||||
from .base import BasePage
|
||||
|
||||
|
||||
class IndexPage(FoundationMetadataPageMixin, RoutablePageMixin, Page):
|
||||
class IndexPage(RoutablePageMixin, BasePage):
|
||||
"""
|
||||
This is a page type for creating "index" pages that
|
||||
can show cards for all their child content.
|
||||
|
@ -76,7 +75,6 @@ class IndexPage(FoundationMetadataPageMixin, RoutablePageMixin, Page):
|
|||
def get_context(self, request):
|
||||
# bootstrap the render context
|
||||
context = super().get_context(request)
|
||||
context = set_main_site_nav_information(self, context, "Homepage")
|
||||
context = get_page_tree_information(self, context)
|
||||
|
||||
# perform entry pagination and (optional) filterin
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from django import http, shortcuts
|
||||
from django.db import models
|
||||
from django.utils import text as text_utils
|
||||
from wagtail import images as wagtail_images
|
||||
from wagtail import models as wagtail_models
|
||||
from wagtail.admin.panels import FieldPanel
|
||||
|
@ -9,17 +8,23 @@ from wagtail_localize.fields import SynchronizedField, TranslatableField
|
|||
|
||||
from networkapi.wagtailpages import utils
|
||||
from networkapi.wagtailpages.pagemodels import profiles
|
||||
from networkapi.wagtailpages.pagemodels.research_hub import base as research_base
|
||||
from networkapi.wagtailpages.pagemodels.research_hub import detail_page, library_page
|
||||
from networkapi.wagtailpages.pagemodels.base import BasePage
|
||||
from networkapi.wagtailpages.pagemodels.libraries.research_hub import (
|
||||
detail_page,
|
||||
library_page,
|
||||
)
|
||||
|
||||
|
||||
class ResearchAuthorsIndexPage(
|
||||
routable_models.RoutablePageMixin,
|
||||
research_base.ResearchHubBasePage,
|
||||
BasePage,
|
||||
):
|
||||
max_count = 1
|
||||
|
||||
parent_page_types = ["ResearchLandingPage"]
|
||||
|
||||
template = "pages/research_hub/authors_index_page.html"
|
||||
|
||||
banner_image = models.ForeignKey(
|
||||
wagtail_images.get_image_model_string(),
|
||||
null=True,
|
||||
|
@ -48,47 +53,40 @@ class ResearchAuthorsIndexPage(
|
|||
def get_context(self, request):
|
||||
context = super().get_context(request)
|
||||
author_profiles = profiles.Profile.objects.all()
|
||||
author_profiles = author_profiles.filter_research_authors()
|
||||
author_profiles = utils.get_research_authors(author_profiles)
|
||||
# When the index is displayed in a non-default locale, then want to show
|
||||
# the profile associated with that locale. But, profiles do not necessarily
|
||||
# exist in all locales. We prefer showing the profile for the locale, but fall
|
||||
# back to the profile on the default locale.
|
||||
author_profiles = utils.localize_queryset(author_profiles)
|
||||
context["author_profiles"] = author_profiles
|
||||
context["breadcrumbs"] = self.get_breadcrumbs()
|
||||
return context
|
||||
|
||||
@routable_models.route(r"^(?P<profile_id>[0-9]+)/(?P<profile_slug>[-a-z]+)/$")
|
||||
@routable_models.route(r"^(?P<profile_slug>[-a-z0-9]+)/$", name="wagtailpages:research-author-detail")
|
||||
def author_detail(
|
||||
self,
|
||||
request: http.HttpRequest,
|
||||
profile_id: str,
|
||||
profile_slug: str,
|
||||
):
|
||||
context_overrides = self.get_author_detail_context(profile_id=int(profile_id))
|
||||
|
||||
slugified_profile_name = text_utils.slugify(context_overrides["author_profile"].name)
|
||||
if not slugified_profile_name == profile_slug:
|
||||
raise http.Http404("Slug does not fit profile name")
|
||||
context_overrides = self.get_author_detail_context(profile_slug=profile_slug)
|
||||
|
||||
return self.render(
|
||||
request=request,
|
||||
template="wagtailpages/research_author_detail_page.html",
|
||||
template="pages/research_hub/author_detail_page.html",
|
||||
context_overrides=context_overrides,
|
||||
)
|
||||
|
||||
def get_author_detail_context(self, profile_id: int):
|
||||
research_author_profiles = profiles.Profile.objects.filter_research_authors()
|
||||
def get_author_detail_context(self, profile_slug: str):
|
||||
author_profiles = utils.localize_queryset(utils.get_research_authors(profiles.Profile.objects.all()))
|
||||
author_profile = shortcuts.get_object_or_404(
|
||||
research_author_profiles,
|
||||
id=profile_id,
|
||||
author_profiles,
|
||||
slug=profile_slug,
|
||||
)
|
||||
|
||||
return {
|
||||
"author_profile": author_profile,
|
||||
"author_research_count": self.get_author_research_count(author_profile=author_profile),
|
||||
# On author detail pages to include the link to the authors index.
|
||||
"breadcrumbs": self.get_breadcrumbs(include_self=True),
|
||||
"latest_research": self.get_latest_author_research(author_profile=author_profile),
|
||||
"library_page": library_page.ResearchLibraryPage.objects.first(),
|
||||
}
|
|
@ -12,20 +12,24 @@ from wagtail.images import edit_handlers as image_handlers
|
|||
from wagtail.search import index
|
||||
from wagtail_localize import fields as localize_fields
|
||||
|
||||
from networkapi.wagtailpages.pagemodels.base import BasePage
|
||||
from networkapi.wagtailpages.pagemodels.customblocks.base_rich_text_options import (
|
||||
base_rich_text_options,
|
||||
)
|
||||
from networkapi.wagtailpages.pagemodels.research_hub import authors_index
|
||||
from networkapi.wagtailpages.pagemodels.research_hub import base as research_base
|
||||
from networkapi.wagtailpages.pagemodels.libraries.research_hub import authors_index
|
||||
from networkapi.wagtailpages.pagemodels.profiles import Profile
|
||||
from networkapi.wagtailpages.utils import localize_queryset
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ResearchDetailPage(research_base.ResearchHubBasePage):
|
||||
class ResearchDetailPage(BasePage):
|
||||
parent_page_types = ["ResearchLibraryPage"]
|
||||
|
||||
subpage_types = ["ArticlePage", "PublicationPage"]
|
||||
|
||||
template = "pages/research_hub/detail_page.html"
|
||||
|
||||
cover_image = models.ForeignKey(
|
||||
wagtail_images.get_image_model_string(),
|
||||
null=True,
|
||||
|
@ -132,10 +136,16 @@ class ResearchDetailPage(research_base.ResearchHubBasePage):
|
|||
|
||||
def get_context(self, request):
|
||||
context = super().get_context(request)
|
||||
context["breadcrumbs"] = self.get_breadcrumbs()
|
||||
context["authors_index"] = authors_index.ResearchAuthorsIndexPage.objects.first()
|
||||
context["research_authors"] = self.get_research_authors()
|
||||
return context
|
||||
|
||||
def get_research_authors(self):
|
||||
research_author_profiles = localize_queryset(
|
||||
Profile.objects.prefetch_related("authored_research").filter(authored_research__research_detail_page=self)
|
||||
)
|
||||
return research_author_profiles
|
||||
|
||||
def get_research_author_names(self):
|
||||
return [ra.author_profile.name for ra in self.research_authors.all()]
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
from django import forms
|
||||
from django.utils.translation import pgettext_lazy
|
||||
|
||||
from networkapi.wagtailpages import utils
|
||||
from networkapi.wagtailpages.pagemodels import profiles as profile_models
|
||||
from networkapi.wagtailpages.pagemodels.libraries.research_hub import (
|
||||
detail_page,
|
||||
taxonomies,
|
||||
)
|
||||
|
||||
|
||||
def _get_author_options():
|
||||
author_profiles = utils.get_research_authors(profile_models.Profile.objects.all())
|
||||
author_profiles = utils.localize_queryset(author_profiles)
|
||||
return [(author_profile.id, author_profile.name) for author_profile in author_profiles]
|
||||
|
||||
|
||||
def _get_topic_options():
|
||||
topics = taxonomies.ResearchTopic.objects.all()
|
||||
topics = utils.localize_queryset(topics)
|
||||
return [(topic.id, topic.name) for topic in topics]
|
||||
|
||||
|
||||
def _get_region_options():
|
||||
regions = taxonomies.ResearchRegion.objects.all()
|
||||
regions = utils.localize_queryset(regions)
|
||||
return [(region.id, region.name) for region in regions]
|
||||
|
||||
|
||||
def _get_year_options():
|
||||
dates = detail_page.ResearchDetailPage.objects.dates(
|
||||
"original_publication_date",
|
||||
"year",
|
||||
order="DESC",
|
||||
)
|
||||
year_options = [(date.year, date.year) for date in dates]
|
||||
empty_option = (
|
||||
"",
|
||||
pgettext_lazy("Option in a list of years", "Any"),
|
||||
)
|
||||
return [empty_option] + year_options
|
||||
|
||||
|
||||
class LibraryPageFilterForm(forms.Form):
|
||||
topic = forms.MultipleChoiceField(
|
||||
required=False,
|
||||
widget=forms.CheckboxSelectMultiple(attrs={"class": "rh-checkbox"}),
|
||||
choices=_get_topic_options,
|
||||
label=pgettext_lazy("Filter form field label", "Topics"),
|
||||
)
|
||||
year = forms.ChoiceField(
|
||||
required=False,
|
||||
choices=_get_year_options,
|
||||
widget=forms.RadioSelect(attrs={"class": "rh-radio"}),
|
||||
label=pgettext_lazy("Filter form field label", "Publication Date"),
|
||||
)
|
||||
author = forms.MultipleChoiceField(
|
||||
required=False,
|
||||
widget=forms.CheckboxSelectMultiple(attrs={"class": "rh-checkbox"}),
|
||||
choices=_get_author_options,
|
||||
label=pgettext_lazy("Filter form field label", "Authors"),
|
||||
)
|
||||
region = forms.MultipleChoiceField(
|
||||
required=False,
|
||||
widget=forms.CheckboxSelectMultiple(attrs={"class": "rh-checkbox"}),
|
||||
choices=_get_region_options,
|
||||
label=pgettext_lazy("Filter form field label", "Regions"),
|
||||
)
|
|
@ -4,16 +4,19 @@ from wagtail import models as wagtail_models
|
|||
from wagtail.admin.panels import FieldPanel, InlinePanel
|
||||
from wagtail_localize.fields import SynchronizedField, TranslatableField
|
||||
|
||||
from networkapi.wagtailpages.pagemodels.research_hub import base as research_base
|
||||
from networkapi.wagtailpages.pagemodels.base import BasePage
|
||||
|
||||
|
||||
class ResearchLandingPage(research_base.ResearchHubBasePage):
|
||||
class ResearchLandingPage(BasePage):
|
||||
max_count = 1
|
||||
|
||||
subpage_types = [
|
||||
"ResearchLibraryPage",
|
||||
"ResearchAuthorsIndexPage",
|
||||
]
|
||||
|
||||
template = "pages/research_hub/landing_page.html"
|
||||
|
||||
intro = models.CharField(
|
||||
blank=True,
|
||||
max_length=250,
|
|
@ -3,7 +3,6 @@ from typing import Optional
|
|||
|
||||
from django.core import paginator
|
||||
from django.db import models
|
||||
from django.utils.translation import pgettext_lazy
|
||||
from wagtail import images as wagtail_images
|
||||
from wagtail import models as wagtail_models
|
||||
from wagtail.admin import panels
|
||||
|
@ -12,22 +11,32 @@ from wagtail_localize.fields import SynchronizedField, TranslatableField
|
|||
|
||||
from networkapi.wagtailpages import utils
|
||||
from networkapi.wagtailpages.pagemodels import profiles as profile_models
|
||||
from networkapi.wagtailpages.pagemodels.research_hub import base as research_base
|
||||
from networkapi.wagtailpages.pagemodels.research_hub import (
|
||||
from networkapi.wagtailpages.pagemodels.base import BasePage
|
||||
from networkapi.wagtailpages.pagemodels.libraries.research_hub import (
|
||||
constants,
|
||||
detail_page,
|
||||
taxonomies,
|
||||
)
|
||||
from networkapi.wagtailpages.pagemodels.libraries.research_hub.forms import (
|
||||
LibraryPageFilterForm,
|
||||
)
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from django import http, template
|
||||
from django import http
|
||||
from django import template as django_template
|
||||
|
||||
|
||||
class ResearchLibraryPage(research_base.ResearchHubBasePage):
|
||||
class ResearchLibraryPage(BasePage):
|
||||
max_count = 1
|
||||
|
||||
parent_page_types = ["ResearchLandingPage"]
|
||||
|
||||
subpage_types = ["ResearchDetailPage"]
|
||||
|
||||
template = "pages/research_hub/library_page.html"
|
||||
|
||||
SORT_CHOICES = constants.SORT_CHOICES
|
||||
|
||||
banner_image = models.ForeignKey(
|
||||
wagtail_images.get_image_model_string(),
|
||||
null=True,
|
||||
|
@ -40,11 +49,11 @@ class ResearchLibraryPage(research_base.ResearchHubBasePage):
|
|||
help_text="Maximum number of results to be displayed per page.",
|
||||
)
|
||||
|
||||
content_panels = research_base.ResearchHubBasePage.content_panels + [
|
||||
content_panels = BasePage.content_panels + [
|
||||
image_panels.FieldPanel("banner_image"),
|
||||
]
|
||||
|
||||
settings_panels = research_base.ResearchHubBasePage.settings_panels + [panels.FieldPanel("results_count")]
|
||||
settings_panels = BasePage.settings_panels + [panels.FieldPanel("results_count")]
|
||||
|
||||
translatable_fields = [
|
||||
# Content tab fields
|
||||
|
@ -57,24 +66,24 @@ class ResearchLibraryPage(research_base.ResearchHubBasePage):
|
|||
SynchronizedField("search_image"),
|
||||
]
|
||||
|
||||
def get_context(self, request: "http.HttpRequest") -> "template.Context":
|
||||
def get_context(self, request: "http.HttpRequest") -> "django_template.Context":
|
||||
search_query: str = request.GET.get("search", "")
|
||||
sort_value: str = request.GET.get("sort", "")
|
||||
sort: constants.SortOption = constants.SORT_CHOICES.get(sort_value, constants.SORT_NEWEST_FIRST)
|
||||
filtered_author_ids: list[int] = [int(author_id) for author_id in request.GET.getlist("author")]
|
||||
filtered_topic_ids: list[int] = [int(topic_id) for topic_id in request.GET.getlist("topic")]
|
||||
filtered_region_ids: list[int] = [int(region_id) for region_id in request.GET.getlist("region")]
|
||||
# Because a research detail page can only have a single publication date, we
|
||||
# can also only select a single one. Otherwise we would filter for pages with
|
||||
# two publication dates which does not exist.
|
||||
year_parameter: str = request.GET.get("year", "")
|
||||
filtered_year: Optional[int]
|
||||
try:
|
||||
filtered_year = int(year_parameter)
|
||||
except ValueError:
|
||||
# Any non-number year parameter will trigger the ValueError.
|
||||
filtered_year = None
|
||||
page: Optional[str] = request.GET.get("page")
|
||||
|
||||
filter_form = LibraryPageFilterForm(request.GET, label_suffix="")
|
||||
if not filter_form.is_valid():
|
||||
# If the form is not valid, we will not filter by any of the values.
|
||||
# This will result in all research being displayed.
|
||||
filtered_author_ids: list[int] = []
|
||||
filtered_topic_ids: list[int] = []
|
||||
filtered_region_ids: list[int] = []
|
||||
filtered_year: Optional[int] = None
|
||||
|
||||
filtered_author_ids = filter_form.cleaned_data["author"]
|
||||
filtered_topic_ids = filter_form.cleaned_data["topic"]
|
||||
filtered_region_ids = filter_form.cleaned_data["region"]
|
||||
filtered_year = filter_form.cleaned_data["year"]
|
||||
|
||||
searched_and_filtered_research_detail_pages = self._get_research_detail_pages(
|
||||
search=search_query,
|
||||
|
@ -89,81 +98,18 @@ class ResearchLibraryPage(research_base.ResearchHubBasePage):
|
|||
per_page=self.results_count,
|
||||
allow_empty_first_page=True,
|
||||
)
|
||||
|
||||
page: Optional[str] = request.GET.get("page")
|
||||
research_detail_pages_page = research_detail_pages_paginator.get_page(page)
|
||||
|
||||
context: "template.Context" = super().get_context(request)
|
||||
context["breadcrumbs"] = self.get_breadcrumbs()
|
||||
context: "django_template.Context" = super().get_context(request)
|
||||
context["search_query"] = search_query
|
||||
context["sort"] = sort
|
||||
context["author_options"] = self._get_author_options()
|
||||
context["filtered_author_ids"] = filtered_author_ids
|
||||
context["topic_options"] = self._get_topic_options()
|
||||
context["filtered_topic_ids"] = filtered_topic_ids
|
||||
context["region_options"] = self._get_region_options()
|
||||
context["filtered_region_ids"] = filtered_region_ids
|
||||
context["year_options"] = self._get_year_options()
|
||||
context["filtered_year"] = filtered_year
|
||||
context["form"] = filter_form
|
||||
context["research_detail_pages_count"] = research_detail_pages_paginator.count
|
||||
context["research_detail_pages"] = research_detail_pages_page
|
||||
return context
|
||||
|
||||
def _get_author_options(self):
|
||||
author_profiles = profile_models.Profile.objects.filter_research_authors()
|
||||
author_profiles = utils.localize_queryset(author_profiles)
|
||||
return [
|
||||
{
|
||||
"id": author_profile.id,
|
||||
"value": author_profile.id,
|
||||
"label": author_profile.name,
|
||||
}
|
||||
for author_profile in author_profiles
|
||||
]
|
||||
|
||||
def _get_topic_options(self):
|
||||
topics = taxonomies.ResearchTopic.objects.all()
|
||||
topics = utils.localize_queryset(topics)
|
||||
return [
|
||||
{
|
||||
"id": topic.id,
|
||||
"value": topic.id,
|
||||
"label": topic.name,
|
||||
}
|
||||
for topic in topics
|
||||
]
|
||||
|
||||
def _get_region_options(self):
|
||||
regions = taxonomies.ResearchRegion.objects.all()
|
||||
regions = utils.localize_queryset(regions)
|
||||
return [
|
||||
{
|
||||
"id": region.id,
|
||||
"value": region.id,
|
||||
"label": region.name,
|
||||
}
|
||||
for region in regions
|
||||
]
|
||||
|
||||
def _get_year_options(self):
|
||||
dates = detail_page.ResearchDetailPage.objects.dates(
|
||||
"original_publication_date",
|
||||
"year",
|
||||
order="DESC",
|
||||
)
|
||||
year_options = [
|
||||
{
|
||||
"id": date.year,
|
||||
"value": date.year,
|
||||
"label": date.year,
|
||||
}
|
||||
for date in dates
|
||||
]
|
||||
empty_option = {
|
||||
"id": "any",
|
||||
"value": "",
|
||||
"label": pgettext_lazy("Option in a list of years", "Any"),
|
||||
}
|
||||
return [empty_option] + year_options
|
||||
|
||||
def _get_research_detail_pages(
|
||||
self,
|
||||
*,
|
||||
|
@ -181,7 +127,7 @@ class ResearchLibraryPage(research_base.ResearchHubBasePage):
|
|||
research_detail_pages = detail_page.ResearchDetailPage.objects.live().public()
|
||||
research_detail_pages = research_detail_pages.filter(locale=wagtail_models.Locale.get_active())
|
||||
|
||||
author_profiles = profile_models.Profile.objects.filter_research_authors()
|
||||
author_profiles = utils.get_research_authors(profile_models.Profile.objects.all())
|
||||
author_profiles = author_profiles.filter(id__in=author_profile_ids)
|
||||
for author_profile in author_profiles:
|
||||
# Synced but not translated pages are still associated with the default
|
|
@ -0,0 +1,19 @@
|
|||
from django.db import models
|
||||
from wagtail.admin import panels as edit_handlers
|
||||
from wagtail.snippets import models as snippet_models
|
||||
|
||||
from networkapi.wagtailpages.pagemodels.taxonomy import BaseTaxonomy
|
||||
|
||||
|
||||
@snippet_models.register_snippet
|
||||
class ResearchRegion(BaseTaxonomy):
|
||||
pass
|
||||
|
||||
|
||||
@snippet_models.register_snippet
|
||||
class ResearchTopic(BaseTaxonomy):
|
||||
description = models.TextField(null=False, blank=True)
|
||||
|
||||
panels = BaseTaxonomy.panels + [
|
||||
edit_handlers.FieldPanel("description"),
|
||||
]
|
|
@ -0,0 +1,11 @@
|
|||
from networkapi.wagtailpages import utils
|
||||
|
||||
|
||||
class FoundationNavigationPageMixin:
|
||||
def get_context(self, request):
|
||||
context = super().get_context(request)
|
||||
context = utils.set_main_site_nav_information(self, context, "Homepage")
|
||||
return context
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
|
@ -4,12 +4,12 @@ from wagtail.fields import StreamField
|
|||
from wagtail.models import Page
|
||||
from wagtail_localize.fields import SynchronizedField, TranslatableField
|
||||
|
||||
from ..utils import get_page_tree_information, set_main_site_nav_information
|
||||
from ..utils import get_page_tree_information
|
||||
from .base import BasePage
|
||||
from .customblocks.base_fields import base_fields
|
||||
from .mixin.foundation_metadata import FoundationMetadataPageMixin
|
||||
|
||||
|
||||
class ModularPage(FoundationMetadataPageMixin, Page):
|
||||
class ModularPage(BasePage):
|
||||
"""
|
||||
This base class offers universal component picking.
|
||||
Note: this is a legacy class, see
|
||||
|
@ -65,10 +65,6 @@ class ModularPage(FoundationMetadataPageMixin, Page):
|
|||
|
||||
show_in_menus_default = True
|
||||
|
||||
def get_context(self, request):
|
||||
context = super().get_context(request)
|
||||
return set_main_site_nav_information(self, context, "Homepage")
|
||||
|
||||
|
||||
class MiniSiteNameSpace(ModularPage):
|
||||
subpage_types = [
|
||||
|
|
|
@ -1,106 +0,0 @@
|
|||
from django.db import models
|
||||
from wagtail.admin.panels import FieldPanel, MultiFieldPanel
|
||||
from wagtail.fields import StreamField
|
||||
from wagtail.models import Page
|
||||
from wagtail_localize.fields import SynchronizedField, TranslatableField
|
||||
|
||||
from ..utils import get_page_tree_information, set_main_site_nav_information
|
||||
from .customblocks.base_fields import base_fields
|
||||
from .mixin.foundation_banner_inheritance import FoundationBannerInheritanceMixin
|
||||
from .mixin.foundation_metadata import FoundationMetadataPageMixin
|
||||
|
||||
|
||||
class PrimaryPage(FoundationMetadataPageMixin, FoundationBannerInheritanceMixin, Page):
|
||||
"""
|
||||
Basically a straight copy of modular page, but with
|
||||
restrictions on what can live 'under it'.
|
||||
|
||||
Ideally this is just PrimaryPage(ModularPage) but
|
||||
setting that up as a migration seems to be causing
|
||||
problems.
|
||||
"""
|
||||
|
||||
header = models.CharField(max_length=250, blank=True)
|
||||
|
||||
banner = models.ForeignKey(
|
||||
"wagtailimages.Image",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="primary_banner",
|
||||
verbose_name="Hero Image",
|
||||
help_text="Choose an image that's bigger than 4032px x 1152px with aspect ratio 3.5:1",
|
||||
)
|
||||
|
||||
intro = models.CharField(
|
||||
max_length=350,
|
||||
blank=True,
|
||||
help_text="Intro paragraph to show in hero cutout box",
|
||||
)
|
||||
|
||||
narrowed_page_content = models.BooleanField(
|
||||
default=False,
|
||||
help_text="For text-heavy pages, turn this on to reduce the overall width of the content on the page.",
|
||||
)
|
||||
|
||||
zen_nav = models.BooleanField(
|
||||
default=False,
|
||||
help_text="For secondary nav pages, use this to collapse the primary nav under a toggle hamburger.",
|
||||
)
|
||||
|
||||
body = StreamField(base_fields, use_json_field=True)
|
||||
|
||||
settings_panels = Page.settings_panels + [
|
||||
MultiFieldPanel(
|
||||
[
|
||||
FieldPanel("narrowed_page_content"),
|
||||
],
|
||||
classname="collapsible",
|
||||
),
|
||||
MultiFieldPanel(
|
||||
[
|
||||
FieldPanel("zen_nav"),
|
||||
],
|
||||
classname="collapsible",
|
||||
),
|
||||
]
|
||||
|
||||
content_panels = Page.content_panels + [
|
||||
FieldPanel("header"),
|
||||
FieldPanel("banner"),
|
||||
FieldPanel("intro"),
|
||||
FieldPanel("body"),
|
||||
]
|
||||
|
||||
translatable_fields = [
|
||||
# Promote tab fields
|
||||
SynchronizedField("slug"),
|
||||
TranslatableField("seo_title"),
|
||||
SynchronizedField("show_in_menus"),
|
||||
TranslatableField("search_description"),
|
||||
SynchronizedField("search_image"),
|
||||
# Content tab fields
|
||||
TranslatableField("title"),
|
||||
TranslatableField("header"),
|
||||
SynchronizedField("banner"),
|
||||
TranslatableField("intro"),
|
||||
TranslatableField("body"),
|
||||
SynchronizedField("narrowed_page_content"),
|
||||
SynchronizedField("zen_nav"),
|
||||
]
|
||||
|
||||
subpage_types = [
|
||||
"PrimaryPage",
|
||||
"RedirectingPage",
|
||||
"BanneredCampaignPage",
|
||||
"OpportunityPage",
|
||||
"ArticlePage",
|
||||
]
|
||||
|
||||
show_in_menus_default = True
|
||||
|
||||
def get_context(self, request):
|
||||
context = super().get_context(request)
|
||||
context = set_main_site_nav_information(self, context, "Homepage")
|
||||
context = get_page_tree_information(self, context)
|
||||
return context
|
|
@ -1,4 +1,6 @@
|
|||
from django.db import models
|
||||
from django.db.models import F, Value
|
||||
from django.db.models.functions import Concat
|
||||
from django.utils.text import slugify
|
||||
from wagtail.admin.panels import FieldPanel
|
||||
from wagtail.models import TranslatableMixin
|
||||
|
@ -7,14 +9,6 @@ from wagtail.snippets.models import register_snippet
|
|||
from wagtail_localize.fields import SynchronizedField, TranslatableField
|
||||
|
||||
|
||||
class ProfileQuerySet(models.QuerySet):
|
||||
def filter_research_authors(self):
|
||||
return self.filter(authored_research__isnull=False).distinct()
|
||||
|
||||
def filter_blog_authors(self):
|
||||
return self.filter(blogauthors__isnull=False).distinct()
|
||||
|
||||
|
||||
@register_snippet
|
||||
class Profile(index.Indexed, TranslatableMixin, models.Model):
|
||||
name = models.CharField(max_length=70, blank=False)
|
||||
|
@ -57,14 +51,13 @@ class Profile(index.Indexed, TranslatableMixin, models.Model):
|
|||
index.FilterField("locale_id"),
|
||||
]
|
||||
|
||||
objects = ProfileQuerySet.as_manager()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.slug = slugify(f"{self.name}-{str(self.id)}")
|
||||
self.slug = slugify(self.name)
|
||||
super(Profile, self).save(*args, **kwargs)
|
||||
self._meta.model.objects.filter(id=self.id).update(slug=Concat(F("slug"), Value("-"), F("id")))
|
||||
|
||||
class Meta(TranslatableMixin.Meta):
|
||||
ordering = ["name"]
|
||||
|
|
|
@ -10,14 +10,10 @@ from wagtail_localize.fields import SynchronizedField, TranslatableField
|
|||
|
||||
from networkapi.wagtailpages.pagemodels.profiles import Profile
|
||||
from networkapi.wagtailpages.pagemodels.publications.publication import PublicationPage
|
||||
from networkapi.wagtailpages.utils import (
|
||||
TitleWidget,
|
||||
get_plaintext_titles,
|
||||
set_main_site_nav_information,
|
||||
)
|
||||
from networkapi.wagtailpages.utils import TitleWidget, get_plaintext_titles
|
||||
|
||||
from ..article_fields import article_fields
|
||||
from ..mixin.foundation_metadata import FoundationMetadataPageMixin
|
||||
from ..base import BasePage
|
||||
|
||||
|
||||
class ArticleAuthors(Orderable):
|
||||
|
@ -35,7 +31,7 @@ class ArticleAuthors(Orderable):
|
|||
return self.author.name
|
||||
|
||||
|
||||
class ArticlePage(FoundationMetadataPageMixin, Page):
|
||||
class ArticlePage(BasePage):
|
||||
"""
|
||||
Articles can belong to any page in the Wagtail Tree.
|
||||
An ArticlePage can have no children
|
||||
|
@ -320,4 +316,4 @@ class ArticlePage(FoundationMetadataPageMixin, Page):
|
|||
# we need access to the `request` object
|
||||
# menu_items is required for zen_nav in the templates
|
||||
context["get_titles"] = get_plaintext_titles(request, self.body, "content")
|
||||
return set_main_site_nav_information(self, context, "Homepage")
|
||||
return context
|
||||
|
|
|
@ -9,10 +9,9 @@ from wagtail_color_panel.fields import ColorField
|
|||
from wagtail_localize.fields import SynchronizedField, TranslatableField
|
||||
|
||||
from networkapi.wagtailpages.pagemodels.profiles import Profile
|
||||
from networkapi.wagtailpages.utils import set_main_site_nav_information
|
||||
|
||||
from ..base import BasePage
|
||||
from ..customblocks.base_rich_text_options import base_rich_text_options
|
||||
from ..mixin.foundation_metadata import FoundationMetadataPageMixin
|
||||
|
||||
|
||||
class PublicationAuthors(Orderable):
|
||||
|
@ -30,7 +29,7 @@ class PublicationAuthors(Orderable):
|
|||
return f"Author: {self.author.name}"
|
||||
|
||||
|
||||
class PublicationPage(FoundationMetadataPageMixin, Page):
|
||||
class PublicationPage(BasePage):
|
||||
"""
|
||||
This is the root page of a publication.
|
||||
|
||||
|
@ -308,4 +307,4 @@ class PublicationPage(FoundationMetadataPageMixin, Page):
|
|||
# User is not logged in AND this page is live. Only fetch live grandchild pages.
|
||||
pages.append({"child": page, "grandchildren": page.get_children().live()})
|
||||
context["child_pages"] = pages
|
||||
return set_main_site_nav_information(self, context, "Homepage")
|
||||
return context
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
from django.apps import apps
|
||||
from wagtail import models as wagtail_models
|
||||
|
||||
from networkapi.wagtailpages import utils
|
||||
from networkapi.wagtailpages.pagemodels.mixin import foundation_metadata
|
||||
|
||||
|
||||
class ResearchHubBasePage(
|
||||
foundation_metadata.FoundationMetadataPageMixin,
|
||||
wagtail_models.Page,
|
||||
):
|
||||
def get_context(self, request):
|
||||
context = super().get_context(request)
|
||||
context = utils.set_main_site_nav_information(self, context, "Homepage")
|
||||
return context
|
||||
|
||||
def get_breadcrumbs(self, include_self=False):
|
||||
ResearchLandingPageModel = apps.get_model("wagtailpages", "ResearchLandingPage")
|
||||
research_landing_page = self.get_ancestors().type(ResearchLandingPageModel).first()
|
||||
page_ancestors = self.get_ancestors(include_self).descendant_of(research_landing_page, True)
|
||||
breadcrumb_list = [
|
||||
{"title": ancestor_page.title, "url": ancestor_page.url} for ancestor_page in page_ancestors
|
||||
]
|
||||
|
||||
return breadcrumb_list
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче