Merge branch 'main' into dependabot/pip/psycopg2-binary-2.9.6

This commit is contained in:
Daniel Miranda 2023-06-26 09:05:01 -07:00 коммит произвёл GitHub
Родитель eddcae450f 1af651384a
Коммит 43e95a63c7
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
166 изменённых файлов: 11756 добавлений и 3138 удалений

10
.coveragerc Normal file
Просмотреть файл

@ -0,0 +1,10 @@
[coverage:run]
source = netwrok-api/networkapi
omit =
*migrations*
*settings.py*
*test_*
branch = true
[coverage:report]
show_missing = True

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

11
.github/workflows/continous-integration.yml поставляемый
Просмотреть файл

@ -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

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

@ -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

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

@ -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="">&times;</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 &#x27;{self.article_page_under_index_page}&#x27; 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 &#x27;{self.fr_article_page_under_index_page}&#x27; 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 &#x27;{self.de_article_page_under_index_page}&#x27; 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

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше