Merge branch 'main' into playwright-improvements
This commit is contained in:
Коммит
3b40240e98
|
@ -0,0 +1,10 @@
|
|||
[coverage:run]
|
||||
source = netwrok-api/networkapi
|
||||
omit =
|
||||
*migrations*
|
||||
*settings.py*
|
||||
*test_*
|
||||
branch = true
|
||||
|
||||
[coverage:report]
|
||||
show_missing = True
|
|
@ -86,6 +86,8 @@ jobs:
|
|||
run: |
|
||||
npm run build
|
||||
python network-api/manage.py collectstatic --no-input --verbosity 0
|
||||
python network-api/manage.py check
|
||||
python network-api/manage.py makemigrations --check --dry-run
|
||||
python network-api/manage.py migrate --no-input
|
||||
python network-api/manage.py block_inventory
|
||||
python network-api/manage.py compilemessages
|
||||
|
@ -105,7 +107,7 @@ jobs:
|
|||
- name: Run type checks
|
||||
run: mypy network-api
|
||||
- name: Run Tests
|
||||
run: coverage run --source './network-api/networkapi' network-api/manage.py test networkapi
|
||||
run: cd network-api && pytest --ds=networkapi.settings -cov=network-api/networkapi --cov-report=term-missing
|
||||
- name: Coveralls
|
||||
run: coveralls
|
||||
continue-on-error: true
|
||||
|
|
14
README.md
14
README.md
|
@ -77,12 +77,24 @@ 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.
|
||||
|
|
|
@ -10,3 +10,9 @@ mypy
|
|||
ptvsd
|
||||
types-python-slugify
|
||||
types-requests
|
||||
|
||||
pytest
|
||||
pytest-django
|
||||
pytest-cov
|
||||
pytest-xdist
|
||||
pytest-sugar
|
|
@ -10,7 +10,7 @@ asgiref==3.5.0
|
|||
# django
|
||||
black==22.10.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.18
|
||||
# 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,6 +73,8 @@ idna==3.3
|
|||
# -c requirements.txt
|
||||
# requests
|
||||
# urllib3
|
||||
iniconfig==2.0.0
|
||||
# via pytest
|
||||
isort==5.10.1
|
||||
# via -r dev-requirements.in
|
||||
jsbeautifier==1.14.7
|
||||
|
@ -81,12 +89,19 @@ mypy-extensions==0.4.3
|
|||
# via
|
||||
# black
|
||||
# mypy
|
||||
packaging==21.3
|
||||
# via
|
||||
# -c requirements.txt
|
||||
# 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 +112,29 @@ 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
|
||||
# -c requirements.txt
|
||||
# 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 +152,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 +184,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
|
||||
|
|
|
@ -152,7 +152,7 @@ def signup_submission(request, signup):
|
|||
data["campaign_id"] = cid
|
||||
|
||||
# Subscribing to newsletter using basket.
|
||||
response = basket.subscribe(data["email"], data["newsletters"], lang=data["lang"])
|
||||
response = basket.subscribe(data["email"], data["newsletters"], lang=data["lang"], source_url=data["source"])
|
||||
if response["status"] == "ok":
|
||||
return JsonResponse(data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
@ -222,7 +222,7 @@ 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"])
|
||||
basket.subscribe(data["email"], "mozilla-foundation", lang=data["lang"], source_url=data["source"])
|
||||
data["newsletterSignup"] = False
|
||||
|
||||
return send_to_sqs(crm_sqs["client"], crm_queue_url, message, type="petition")
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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(
|
||||
|
|
|
@ -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 profile_tags wagtailimages_tags %}
|
||||
{% load wagtailroutablepage_tags wagtailimages_tags %}
|
||||
<a
|
||||
href="{% routableprofileurl page=authors_index profile=author_profile url_name="author_detail" %}"
|
||||
href="{% routablepageurl page=authors_index.localized profile_id=author_profile.id profile_slug=author_profile.name|slugify 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 %}">
|
|
@ -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,4 +1,4 @@
|
|||
{% extends "./research_base.html" %}
|
||||
{% extends "./base.html" %}
|
||||
{% load i18n l10n wagtailcore_tags wagtailimages_tags %}
|
||||
|
||||
{% block research_hub_content %}
|
||||
|
@ -20,7 +20,7 @@
|
|||
class="medium:tw-col-start-2"
|
||||
{% endif %}
|
||||
>
|
||||
{% include './fragments/research_breadcrumb.html' with breadcrumb_list=breadcrumbs %}
|
||||
{% include "fragments/breadcrumbs.html" with breadcrumb_list=breadcrumbs %}
|
||||
</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" %}
|
||||
{% extends "./base.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block research_hub_content %}
|
||||
<div>
|
||||
{% include './fragments/research_breadcrumb.html' with breadcrumb_list=breadcrumbs %}
|
||||
{% include 'fragments/breadcrumbs.html' with breadcrumb_list=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" %}
|
||||
{% extends "./base.html" %}
|
||||
{% load i18n wagtailcore_tags wagtailimages_tags %}
|
||||
|
||||
{% 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 %}
|
||||
{% include "fragments/breadcrumbs.html" with breadcrumb_list=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,10 +1,10 @@
|
|||
{% extends "./research_base.html" %}
|
||||
{% extends "./base.html" %}
|
||||
{% load i18n static wagtailcore_tags wagtailimages_tags %}
|
||||
|
||||
|
||||
{% block research_hub_content %}
|
||||
<div>
|
||||
{% include './fragments/research_breadcrumb.html' with breadcrumb_list=breadcrumbs %}
|
||||
{% include 'fragments/breadcrumbs.html' with breadcrumb_list=breadcrumbs %}
|
||||
<h1>{{ page.title }}</h1>
|
||||
</div>
|
||||
|
||||
|
@ -18,7 +18,7 @@
|
|||
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" %}
|
||||
{% include "fragments/research_hub/search_bar.html" %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
@ -34,8 +34,8 @@
|
|||
<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 #}
|
||||
|
@ -72,7 +72,7 @@
|
|||
{# 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>
|
||||
|
@ -109,22 +109,22 @@
|
|||
|
||||
{% 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' %}
|
||||
{% include "fragments/research_hub/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' %}
|
||||
{% include "fragments/research_hub/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' %}
|
||||
{% include "fragments/research_hub/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' %}
|
||||
{% include "fragments/research_hub/filter_group.html" with heading=heading options=region_options|dictsort:'label' checked_option_values=filtered_region_ids field_name='region' %}
|
||||
{% endif %}
|
||||
|
||||
<div class="
|
|
@ -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.")
|
||||
|
|
|
@ -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):
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -18,8 +18,11 @@ class ResearchAuthorsIndexPage(
|
|||
research_base.ResearchHubBasePage,
|
||||
):
|
||||
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,
|
||||
|
@ -58,7 +61,9 @@ class ResearchAuthorsIndexPage(
|
|||
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_id>[0-9]+)/(?P<profile_slug>[-a-z]+)/$", name="wagtailpages:research-author-detail"
|
||||
)
|
||||
def author_detail(
|
||||
self,
|
||||
request: http.HttpRequest,
|
||||
|
@ -73,14 +78,14 @@ class ResearchAuthorsIndexPage(
|
|||
|
||||
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()
|
||||
author_profiles = utils.localize_queryset(profiles.Profile.objects.all().filter_research_authors())
|
||||
author_profile = shortcuts.get_object_or_404(
|
||||
research_author_profiles,
|
||||
author_profiles,
|
||||
id=profile_id,
|
||||
)
|
||||
|
||||
|
|
|
@ -15,8 +15,10 @@ from wagtail_localize import fields as localize_fields
|
|||
from networkapi.wagtailpages.pagemodels.customblocks.base_rich_text_options import (
|
||||
base_rich_text_options,
|
||||
)
|
||||
from networkapi.wagtailpages.pagemodels.profiles import Profile
|
||||
from networkapi.wagtailpages.pagemodels.research_hub import authors_index
|
||||
from networkapi.wagtailpages.pagemodels.research_hub import base as research_base
|
||||
from networkapi.wagtailpages.utils import localize_queryset
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -26,6 +28,8 @@ class ResearchDetailPage(research_base.ResearchHubBasePage):
|
|||
|
||||
subpage_types = ["ArticlePage", "PublicationPage"]
|
||||
|
||||
template = "pages/research_hub/detail_page.html"
|
||||
|
||||
cover_image = models.ForeignKey(
|
||||
wagtail_images.get_image_model_string(),
|
||||
null=True,
|
||||
|
@ -134,8 +138,15 @@ class ResearchDetailPage(research_base.ResearchHubBasePage):
|
|||
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()]
|
||||
|
||||
|
|
|
@ -9,11 +9,14 @@ from networkapi.wagtailpages.pagemodels.research_hub import base as research_bas
|
|||
|
||||
class ResearchLandingPage(research_base.ResearchHubBasePage):
|
||||
max_count = 1
|
||||
|
||||
subpage_types = [
|
||||
"ResearchLibraryPage",
|
||||
"ResearchAuthorsIndexPage",
|
||||
]
|
||||
|
||||
template = "pages/research_hub/landing_page.html"
|
||||
|
||||
intro = models.CharField(
|
||||
blank=True,
|
||||
max_length=250,
|
||||
|
|
|
@ -20,14 +20,19 @@ from networkapi.wagtailpages.pagemodels.research_hub import (
|
|||
)
|
||||
|
||||
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):
|
||||
max_count = 1
|
||||
|
||||
parent_page_types = ["ResearchLandingPage"]
|
||||
|
||||
subpage_types = ["ResearchDetailPage"]
|
||||
|
||||
template = "pages/research_hub/library_page.html"
|
||||
|
||||
banner_image = models.ForeignKey(
|
||||
wagtail_images.get_image_model_string(),
|
||||
null=True,
|
||||
|
@ -57,7 +62,7 @@ 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)
|
||||
|
@ -91,7 +96,7 @@ class ResearchLibraryPage(research_base.ResearchHubBasePage):
|
|||
)
|
||||
research_detail_pages_page = research_detail_pages_paginator.get_page(page)
|
||||
|
||||
context: "template.Context" = super().get_context(request)
|
||||
context: "django_template.Context" = super().get_context(request)
|
||||
context["breadcrumbs"] = self.get_breadcrumbs()
|
||||
context["search_query"] = search_query
|
||||
context["sort"] = sort
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
class="medium:tw-col-start-2"
|
||||
{% endif %}
|
||||
>
|
||||
{% include './fragments/research_breadcrumb.html' with breadcrumb_list=breadcrumbs %}
|
||||
{% include 'fragments/breadcrumbs.html' with breadcrumb_list=breadcrumbs %}
|
||||
</div>
|
||||
{% if author.image %}
|
||||
<div id="image-container" class="tw-place-self-center medium:tw-place-self-start">
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
from django import template
|
||||
from django.utils import text as text_utils
|
||||
from wagtail.contrib.routable_page.templatetags import wagtailroutablepage_tags
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def routableprofileurl(context, *args, page, profile, url_name="profile_route", **kwargs):
|
||||
"""
|
||||
Get URL of the profile route on a given page.
|
||||
|
||||
This is a wrapper around Wagtail's `routablepageurl`. It passes the profiles id as `profile_id`
|
||||
and the slugified profile name as `profile_slug` to the url reveral. That means the urls should
|
||||
expect both these parameters. For example the route decorator could look something like this:
|
||||
|
||||
```
|
||||
@routable_models.route(r'^(?P<profile_id>[0-9]+)/(?P<profile_slug>[-a-z]+)/$')`
|
||||
def profile_route(self, request, profile_id, profile_slug):
|
||||
...
|
||||
```
|
||||
|
||||
"""
|
||||
return wagtailroutablepage_tags.routablepageurl(
|
||||
context=context,
|
||||
page=page,
|
||||
url_name=url_name,
|
||||
profile_id=profile.id,
|
||||
profile_slug=text_utils.slugify(profile.name),
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
|
@ -1,15 +1,20 @@
|
|||
import wagtail_factories
|
||||
from django.core import exceptions
|
||||
from django.utils import translation
|
||||
from django.utils.text import slugify
|
||||
|
||||
from networkapi.wagtailpages.factory.research_hub import (
|
||||
detail_page as detail_page_factory,
|
||||
)
|
||||
from networkapi.wagtailpages.factory.research_hub import relations as relations_factory
|
||||
from networkapi.wagtailpages.models import (
|
||||
ArticlePage,
|
||||
PublicationPage,
|
||||
ResearchDetailPage,
|
||||
)
|
||||
from networkapi.wagtailpages.pagemodels.research_hub import authors_index
|
||||
from networkapi.wagtailpages.tests.research_hub import base as research_test_base
|
||||
from networkapi.wagtailpages.tests.research_hub import utils as research_test_utils
|
||||
|
||||
|
||||
class TestResearchLibraryDetailPage(research_test_base.ResearchHubTestCase):
|
||||
|
@ -33,6 +38,89 @@ class TestResearchLibraryDetailPage(research_test_base.ResearchHubTestCase):
|
|||
self.assertEqual(len(breadcrumbs), 2)
|
||||
self.assertEqual(breadcrumbs, expected_breadcrumbs)
|
||||
|
||||
def test_get_research_authors(self) -> None:
|
||||
"""
|
||||
This method should return the profiles of all the page related research authors.
|
||||
"""
|
||||
page_a_author_profiles = []
|
||||
page_a = detail_page_factory.ResearchDetailPageFactory(parent=self.library_page, research_authors=[])
|
||||
page_b = detail_page_factory.ResearchDetailPageFactory(parent=self.library_page)
|
||||
page_b_author_profile = page_b.research_authors.first().author_profile
|
||||
|
||||
for _ in range(3):
|
||||
research_author_relation = relations_factory.ResearchAuthorRelationFactory(research_detail_page=page_a)
|
||||
page_a_author_profiles.append(research_author_relation.author_profile)
|
||||
|
||||
page_a_research_authors = page_a.get_research_authors()
|
||||
|
||||
self.assertEqual(len(page_a_research_authors), 3)
|
||||
self.assertNotIn(page_b_author_profile, page_a_research_authors)
|
||||
self.assertIn(page_a_author_profiles[0], page_a_research_authors)
|
||||
self.assertIn(page_a_author_profiles[1], page_a_research_authors)
|
||||
self.assertIn(page_a_author_profiles[2], page_a_research_authors)
|
||||
|
||||
def test_get_research_authors_returns_localized_profiles(self) -> None:
|
||||
"""
|
||||
If a related author's profile has a translated version available,
|
||||
this method should return it in the active locale.
|
||||
"""
|
||||
detail_page = detail_page_factory.ResearchDetailPageFactory(parent=self.library_page)
|
||||
profile_en = detail_page.research_authors.first().author_profile
|
||||
self.synchronize_tree()
|
||||
# Translating both the page and the research author profile
|
||||
detail_page_fr = research_test_utils.translate_detail_page(detail_page, self.fr_locale)
|
||||
profile_fr = detail_page_fr.research_authors.first().author_profile
|
||||
|
||||
translation.activate(self.fr_locale.language_code)
|
||||
research_authors_fr = detail_page.localized.get_research_authors()
|
||||
|
||||
self.assertEqual(len(research_authors_fr), 1)
|
||||
self.assertIn(profile_fr, research_authors_fr)
|
||||
self.assertNotIn(profile_en, research_authors_fr)
|
||||
|
||||
def test_get_research_authors_returns_localized_profiles_rendered(self) -> None:
|
||||
"""
|
||||
Similar to the above, but these links get passed to routablepageurl in the template
|
||||
so we can be certain that they come out localized.
|
||||
"""
|
||||
detail_page = detail_page_factory.ResearchDetailPageFactory(parent=self.library_page)
|
||||
self.synchronize_tree()
|
||||
# Translating both the page and the research author profile.
|
||||
detail_page_fr = research_test_utils.translate_detail_page(detail_page, self.fr_locale)
|
||||
profile_fr = detail_page_fr.research_authors.first().author_profile
|
||||
translation.activate(self.fr_locale.language_code)
|
||||
author_index = authors_index.ResearchAuthorsIndexPage.objects.first()
|
||||
fr_author_index = authors_index.ResearchAuthorsIndexPage.objects.filter(locale=self.fr_locale).first()
|
||||
|
||||
# Build a URL to check for in the response.
|
||||
# E.G, /fr/research/authors/1/name-here/
|
||||
fr_author_link = f'href="{fr_author_index.url}{profile_fr.id}/{slugify(profile_fr.name)}/"'
|
||||
en_author_link = f'href="{author_index.url}{profile_fr.id}/{slugify(profile_fr.name)}/"'
|
||||
|
||||
# Request the fr version of the page.
|
||||
response = self.client.get(detail_page_fr.url)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, fr_author_link)
|
||||
self.assertNotContains(response, en_author_link)
|
||||
|
||||
def test_get_research_authors_returns_default_locale(self) -> None:
|
||||
"""
|
||||
If a related research author's profile does not have a translated version available,
|
||||
localized pages should return it in the default locale (English).
|
||||
"""
|
||||
detail_page = detail_page_factory.ResearchDetailPageFactory(
|
||||
parent=self.library_page,
|
||||
)
|
||||
profile_en = detail_page.research_authors.first().author_profile
|
||||
self.synchronize_tree()
|
||||
|
||||
translation.activate(self.fr_locale.language_code)
|
||||
research_authors_fr = detail_page.localized.get_research_authors()
|
||||
|
||||
self.assertEqual(len(research_authors_fr), 1)
|
||||
self.assertIn(profile_en, research_authors_fr)
|
||||
|
||||
|
||||
class TestResearchDetailLink(research_test_base.ResearchHubTestCase):
|
||||
def test_clean_with_url(self) -> None:
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
from os.path import abspath, dirname, join
|
||||
|
||||
from django.test import TestCase
|
||||
from django.utils import translation
|
||||
from django.utils.translation.trans_real import (
|
||||
parse_accept_lang_header as django_parse_accept_lang_header,
|
||||
)
|
||||
from django.utils.translation.trans_real import to_language as django_to_language
|
||||
from wagtail.core.models import Locale
|
||||
from wagtail.images.models import Image
|
||||
from wagtail.models import Collection
|
||||
|
||||
|
@ -13,7 +15,9 @@ from networkapi.wagtailpages import (
|
|||
parse_accept_lang_header,
|
||||
to_language,
|
||||
)
|
||||
from networkapi.wagtailpages.utils import create_wagtail_image
|
||||
from networkapi.wagtailpages.factory.profiles import ProfileFactory
|
||||
from networkapi.wagtailpages.pagemodels.profiles import Profile
|
||||
from networkapi.wagtailpages.utils import create_wagtail_image, localize_queryset
|
||||
|
||||
|
||||
class TestCreateWagtailImageUtility(TestCase):
|
||||
|
@ -90,3 +94,143 @@ class TestDjangoTranslationUtilityOverrides(TestCase):
|
|||
django_parse_accept_lang_header("fy-NL,fy;q=0.5"),
|
||||
(("fy-NL", 1.0), ("fy", 0.5)),
|
||||
)
|
||||
|
||||
|
||||
class TestLocalizeQueryset(TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
(cls.default_locale, _) = Locale.objects.get_or_create(language_code="en")
|
||||
(cls.active_locale, _) = Locale.objects.get_or_create(language_code="fr")
|
||||
|
||||
def test_localize_queryset_with_items_only_in_default_locale(self):
|
||||
"""Tests that the function returns a queryset with only one version of each item
|
||||
when the queryset contains items only in default locale. tags: [happy path]"""
|
||||
# Create items only in default locale
|
||||
item1 = ProfileFactory(locale=self.default_locale)
|
||||
item2 = ProfileFactory(locale=self.default_locale)
|
||||
|
||||
# Override the current language to be the active locale
|
||||
translation.activate(self.default_locale.language_code)
|
||||
|
||||
# Call the function
|
||||
result = localize_queryset(Profile.objects.all())
|
||||
|
||||
# Assert that only one version of each item is returned
|
||||
self.assertEqual(len(result), 2)
|
||||
# Assert that both items are returned
|
||||
self.assertIn(item1, result)
|
||||
self.assertIn(item2, result)
|
||||
|
||||
def test_localize_queryset_with_items_only_in_active_locale(self):
|
||||
"""Tests that the function returns a queryset with only one version of each item
|
||||
when the queryset contains items only in active locale. tags: [happy path]"""
|
||||
# Create items only in default locale
|
||||
item1 = ProfileFactory(locale=self.active_locale)
|
||||
item2 = ProfileFactory(locale=self.active_locale)
|
||||
|
||||
# Override the current language to be the active locale
|
||||
translation.activate(self.active_locale.language_code)
|
||||
|
||||
# Call the function
|
||||
result = localize_queryset(Profile.objects.all())
|
||||
|
||||
# Assert that only one version of each item is returned
|
||||
self.assertEqual(len(result), 2)
|
||||
# Assert that both items are returned
|
||||
self.assertIn(item1, result)
|
||||
self.assertIn(item2, result)
|
||||
|
||||
def test_localize_queryset_with_items_in_both_default_and_active_locales(self):
|
||||
"""Tests that the function returns a queryset with only one version of each item
|
||||
when the queryset contains items in both default and active locales. tags: [happy path]"""
|
||||
# Create items in both default and active locales
|
||||
default = ProfileFactory(locale=self.default_locale)
|
||||
active = ProfileFactory(locale=self.active_locale, translation_key=default.translation_key)
|
||||
|
||||
# Override the current language to be the active locale
|
||||
translation.activate(self.active_locale.language_code)
|
||||
|
||||
# Call the function
|
||||
result = localize_queryset(Profile.objects.all())
|
||||
|
||||
# Assert that only one version of each item is returned
|
||||
self.assertEqual(len(result), 1)
|
||||
# Assert that only the item in the active locale is returned
|
||||
self.assertNotIn(default, result)
|
||||
self.assertIn(active, result)
|
||||
|
||||
def test_localize_queryset_with_multiple_items_in_both_locales(self):
|
||||
"""Tests that the function returns a queryset with only one version of each item
|
||||
when the queryset contains translated and not-translated items. tags: [happy path]"""
|
||||
# Create items in default locale:
|
||||
default1 = ProfileFactory(locale=self.default_locale)
|
||||
default2 = ProfileFactory(locale=self.default_locale)
|
||||
default3 = ProfileFactory(locale=self.default_locale)
|
||||
default4 = ProfileFactory(locale=self.default_locale)
|
||||
|
||||
# Translate some items to active locale:
|
||||
active1 = ProfileFactory(locale=self.active_locale, translation_key=default1.translation_key)
|
||||
active2 = ProfileFactory(locale=self.active_locale, translation_key=default2.translation_key)
|
||||
|
||||
# Override the current language to be the active locale
|
||||
translation.activate(self.active_locale.language_code)
|
||||
|
||||
# Call the function
|
||||
result = localize_queryset(Profile.objects.all())
|
||||
|
||||
# We should see 4 items in the result
|
||||
self.assertEqual(len(result), 4)
|
||||
# For items that have translation, the active locale version should be returned
|
||||
self.assertNotIn(default1, result)
|
||||
self.assertIn(active1, result)
|
||||
self.assertNotIn(default2, result)
|
||||
self.assertIn(active2, result)
|
||||
# For items that don't have translation, the default locale version should be returned
|
||||
self.assertIn(default3, result)
|
||||
self.assertIn(default4, result)
|
||||
|
||||
def test_localize_queryset_can_retrieve_translations(self):
|
||||
"""Tests that when the queryset only contains the items in the default location
|
||||
but there are translations available, that those are retrieved and replace their
|
||||
originals. tags: [edge case]"""
|
||||
# Create items in default locale:
|
||||
default1 = ProfileFactory(locale=self.default_locale)
|
||||
default2 = ProfileFactory(locale=self.default_locale)
|
||||
default3 = ProfileFactory(locale=self.default_locale)
|
||||
default4 = ProfileFactory(locale=self.default_locale)
|
||||
|
||||
# Translate some items to active locale:
|
||||
active1 = ProfileFactory(
|
||||
name=default1.name, locale=self.active_locale, translation_key=default1.translation_key
|
||||
)
|
||||
active2 = ProfileFactory(
|
||||
name=default2.name, locale=self.active_locale, translation_key=default2.translation_key
|
||||
)
|
||||
|
||||
# Override the current language to be the active locale
|
||||
translation.activate(self.active_locale.language_code)
|
||||
|
||||
# Call the function with the default location only
|
||||
result = localize_queryset(Profile.objects.all().filter(locale=self.default_locale))
|
||||
|
||||
# We should see 4 items in the result
|
||||
self.assertEqual(len(result), 4)
|
||||
# For items that have translation, the active locale version should be returned
|
||||
# `localize_queryset` should have retrieved the translations from the active location
|
||||
# and replaced the originals with them
|
||||
self.assertNotIn(default1, result)
|
||||
self.assertIn(active1, result)
|
||||
self.assertNotIn(default2, result)
|
||||
self.assertIn(active2, result)
|
||||
# For items that don't have translation, the default locale version should be returned
|
||||
self.assertIn(default3, result)
|
||||
self.assertIn(default4, result)
|
||||
|
||||
def test_localize_queryset_with_empty_queryset(self):
|
||||
"""Tests that the function returns an empty queryset when the input queryset is empty. tags: [edge case]"""
|
||||
# Call the function with an empty queryset
|
||||
result = localize_queryset(Profile.objects.none())
|
||||
|
||||
# Assert that an empty queryset is returned
|
||||
self.assertEqual(len(result), 0)
|
||||
|
|
|
@ -11,7 +11,7 @@ from django import forms
|
|||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.core.files.images import ImageFile
|
||||
from django.db.models import Case, Count, Q, When
|
||||
from django.db.models import Case, Count, Q, Value, When
|
||||
from django.urls import LocalePrefixPattern, URLResolver
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.text import slugify
|
||||
|
@ -336,6 +336,24 @@ def localize_queryset(queryset):
|
|||
default_locale = Locale.get_default()
|
||||
active_locale = Locale.get_active()
|
||||
|
||||
model = queryset.model
|
||||
queryset_keys = queryset.values_list("translation_key", flat=True)
|
||||
|
||||
# Search for translated instances from model that were not included in the provided queryset
|
||||
# (we need the same annotations/order_bt as the original queryset to be able to do the union later)
|
||||
translated_instances = (
|
||||
model._default_manager.filter(translation_key__in=queryset_keys, locale=active_locale)
|
||||
.annotate(locale_is_default=Value(False))
|
||||
.order_by(
|
||||
"translation_key",
|
||||
"locale_is_default",
|
||||
)
|
||||
.distinct("translation_key")
|
||||
)
|
||||
|
||||
# Exclude the translated instances we found from the original queryset
|
||||
queryset = queryset.exclude(translation_key__in=translated_instances.values_list("translation_key", flat=True))
|
||||
|
||||
queryset = queryset.filter(Q(locale=default_locale) | Q(locale=active_locale))
|
||||
queryset = queryset.annotate(
|
||||
locale_is_default=Case(
|
||||
|
@ -346,8 +364,9 @@ def localize_queryset(queryset):
|
|||
queryset = queryset.order_by(
|
||||
"translation_key",
|
||||
"locale_is_default",
|
||||
)
|
||||
queryset = queryset.distinct("translation_key")
|
||||
).distinct("translation_key")
|
||||
|
||||
queryset = queryset | translated_instances
|
||||
return queryset
|
||||
|
||||
|
||||
|
|
|
@ -58,6 +58,9 @@ ignore="H017"
|
|||
"network-api/networkapi/templates/fragments/buyersguide/research_details.html" = "D018"
|
||||
"network-api/networkapi/templates/fragments/buyersguide/shape_background.html" = "H026"
|
||||
"network-api/networkapi/templates/fragments/buyersguide/wavy_line.html" = "H006"
|
||||
"network-api/networkapi/templates/fragments/research_hub/author_card.html" = "H006"
|
||||
"network-api/networkapi/templates/fragments/research_hub/detail_card.html" = "T032"
|
||||
"network-api/networkapi/templates/fragments/research_hub/filter_button.html" = "T002,T032"
|
||||
"network-api/networkapi/templates/fragments/continuous_scrolling_next.html" = "H020,H021"
|
||||
"network-api/networkapi/templates/fragments/custom_hero_guts.html" = "H006,H008,H013,H014,H023,T032"
|
||||
"network-api/networkapi/templates/fragments/favicons.html" = "T032"
|
||||
|
@ -84,6 +87,12 @@ ignore="H017"
|
|||
"network-api/networkapi/templates/pages/buyersguide/editorial_content_index_page.html" = "T003,T032"
|
||||
"network-api/networkapi/templates/pages/buyersguide/home.html" = "T003,T032"
|
||||
"network-api/networkapi/templates/pages/buyersguide/product_page.html" = "T003,T032"
|
||||
"network-api/networkapi/templates/pages/author_detail_page.html" = "T002"
|
||||
"network-api/networkapi/templates/pages/research_hub/authors_index_page.html" = "T002"
|
||||
"network-api/networkapi/templates/pages/research_hub/base.html" = "T003"
|
||||
"network-api/networkapi/templates/pages/research_hub/detail_page.html" = "H006,T032"
|
||||
"network-api/networkapi/templates/pages/research_hub/landing_page.html" = "H008,T003"
|
||||
"network-api/networkapi/templates/pages/research_hub/library_page.html" = "H008,H014,H023,H026,T002,T032"
|
||||
"network-api/networkapi/templates/pages/publication_page.html" = "T003,T032"
|
||||
"network-api/networkapi/templates/pages/styleguide.html" = "H006,H013,H014,H026,T003,T032"
|
||||
"network-api/networkapi/templates/partials/404.html" = "D018,H006"
|
||||
|
@ -168,9 +177,6 @@ ignore="H017"
|
|||
"network-api/networkapi/wagtailpages/templates/wagtailpages/fragments/quote.html" = "H006"
|
||||
"network-api/networkapi/wagtailpages/templates/wagtailpages/fragments/related_posts.html" = "T003"
|
||||
"network-api/networkapi/wagtailpages/templates/wagtailpages/fragments/related_topics_cutout.html" = "T002"
|
||||
"network-api/networkapi/wagtailpages/templates/wagtailpages/fragments/research_author_card.html" = "H006"
|
||||
"network-api/networkapi/wagtailpages/templates/wagtailpages/fragments/research_detail_card.html" = "T032"
|
||||
"network-api/networkapi/wagtailpages/templates/wagtailpages/fragments/research_filter_button.html" = "T002,T032"
|
||||
"network-api/networkapi/wagtailpages/templates/wagtailpages/fragments/search_bar.html" = "T002"
|
||||
"network-api/networkapi/wagtailpages/templates/wagtailpages/fragments/take_action.html" = "H006,H008,H014"
|
||||
"network-api/networkapi/wagtailpages/templates/wagtailpages/homepage.html" = "H014,T003,T032"
|
||||
|
@ -212,12 +218,6 @@ ignore="H017"
|
|||
"network-api/networkapi/wagtailpages/templates/wagtailpages/pages/youtube_regrets_reporter_extension.html" = "H008,T003"
|
||||
"network-api/networkapi/wagtailpages/templates/wagtailpages/pages/youtube_regrets_reporter_page.html" = "D018,H006,H013,H014,T003"
|
||||
"network-api/networkapi/wagtailpages/templates/wagtailpages/primary_page.html" = "T003"
|
||||
"network-api/networkapi/wagtailpages/templates/wagtailpages/research_author_detail_page.html" = "T002"
|
||||
"network-api/networkapi/wagtailpages/templates/wagtailpages/research_authors_index_page.html" = "T002"
|
||||
"network-api/networkapi/wagtailpages/templates/wagtailpages/research_base.html" = "T003"
|
||||
"network-api/networkapi/wagtailpages/templates/wagtailpages/research_detail_page.html" = "H006,T032"
|
||||
"network-api/networkapi/wagtailpages/templates/wagtailpages/research_landing_page.html" = "H008,T003"
|
||||
"network-api/networkapi/wagtailpages/templates/wagtailpages/research_library_page.html" = "H008,H014,H023,H026,T002,T032"
|
||||
"network-api/networkapi/wagtailpages/templates/wagtailpages/static/initiatives_page.html" = "D018,H021,T003"
|
||||
"network-api/networkapi/wagtailpages/templates/wagtailpages/static/participate_page2.html" = "T003"
|
||||
"network-api/networkapi/wagtailpages/templates/wagtailpages/tags/card-cta.html" = "H026"
|
||||
|
@ -234,3 +234,8 @@ profile = "black"
|
|||
extend_skip = ["network-api/media", "network-api/staticfiles"]
|
||||
skip_gitignore = true
|
||||
known_first_party = ["networkapi"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
DJANGO_SETTINGS_MODULE = "networkapi.settings"
|
||||
pythonpath = "network-api network-api/networkapi"
|
||||
python_files = "tests.py test_*.py *_tests.py"
|
|
@ -2,7 +2,7 @@ basket-client
|
|||
beautifulsoup4
|
||||
boto3
|
||||
cached_property==1.5.2
|
||||
Django==3.2.16
|
||||
Django==3.2.18
|
||||
dj-database-url
|
||||
djangorestframework
|
||||
django-admin-sortable==2.3
|
||||
|
@ -24,7 +24,7 @@ pygit2==1.11.1
|
|||
python-slugify
|
||||
requests
|
||||
social-auth-app-django
|
||||
wagtail==4.1.3
|
||||
wagtail==4.1.4
|
||||
wagtail-color-panel
|
||||
wagtail-factories
|
||||
wagtail-localize==1.3.3
|
||||
|
|
|
@ -10,6 +10,8 @@ asgiref==3.5.0
|
|||
# via
|
||||
# django
|
||||
# scout-apm
|
||||
async-timeout==4.0.2
|
||||
# via redis
|
||||
basket-client==1.0.0
|
||||
# via -r requirements.in
|
||||
beautifulsoup4==4.9.3
|
||||
|
@ -24,7 +26,7 @@ botocore==1.24.12
|
|||
# s3transfer
|
||||
cached-property==1.5.2
|
||||
# via -r requirements.in
|
||||
certifi==2021.10.8
|
||||
certifi==2022.12.7
|
||||
# via
|
||||
# requests
|
||||
# sentry-sdk
|
||||
|
@ -35,7 +37,7 @@ cffi==1.15.0
|
|||
# pygit2
|
||||
charset-normalizer==2.0.12
|
||||
# via requests
|
||||
cryptography==36.0.1
|
||||
cryptography==39.0.1
|
||||
# via
|
||||
# pyopenssl
|
||||
# social-auth-core
|
||||
|
@ -44,11 +46,9 @@ defusedxml==0.7.1
|
|||
# via
|
||||
# python3-openid
|
||||
# social-auth-core
|
||||
deprecated==1.2.13
|
||||
# via redis
|
||||
dj-database-url==1.2.0
|
||||
# via -r requirements.in
|
||||
django==3.2.16
|
||||
django==3.2.18
|
||||
# via
|
||||
# -r requirements.in
|
||||
# dj-database-url
|
||||
|
@ -144,9 +144,7 @@ oauthlib==3.2.2
|
|||
# social-auth-core
|
||||
openpyxl==3.1.0
|
||||
# via wagtail
|
||||
packaging==21.3
|
||||
# via redis
|
||||
pillow==9.0.1
|
||||
pillow==9.5.0
|
||||
# via wagtail
|
||||
polib==1.1.1
|
||||
# via wagtail-localize
|
||||
|
@ -162,10 +160,8 @@ pygit2==1.11.1
|
|||
# wagtail-localize-git
|
||||
pyjwt==2.4.0
|
||||
# via social-auth-core
|
||||
pyopenssl==22.0.0
|
||||
pyopenssl==23.0.0
|
||||
# via urllib3
|
||||
pyparsing==3.0.7
|
||||
# via packaging
|
||||
python-dateutil==2.8.2
|
||||
# via
|
||||
# botocore
|
||||
|
@ -181,7 +177,7 @@ pytz==2021.3
|
|||
# django-modelcluster
|
||||
# djangorestframework
|
||||
# l18n
|
||||
redis==4.1.4
|
||||
redis==4.5.4
|
||||
# via django-redis
|
||||
requests==2.28.2
|
||||
# via
|
||||
|
@ -197,7 +193,7 @@ s3transfer==0.5.2
|
|||
# via boto3
|
||||
scout-apm==2.26.1
|
||||
# via -r requirements.in
|
||||
sentry-sdk==1.5.6
|
||||
sentry-sdk==1.21.0
|
||||
# via -r requirements.in
|
||||
six==1.16.0
|
||||
# via
|
||||
|
@ -213,7 +209,7 @@ social-auth-core==4.2.0
|
|||
# via social-auth-app-django
|
||||
soupsieve==2.3.1
|
||||
# via beautifulsoup4
|
||||
sqlparse==0.4.2
|
||||
sqlparse==0.4.4
|
||||
# via django
|
||||
telepath==0.2
|
||||
# via wagtail
|
||||
|
@ -225,13 +221,15 @@ tqdm==4.63.0
|
|||
# via wagtail-inventory
|
||||
typing-extensions==4.4.0
|
||||
# via wagtail-localize
|
||||
urllib3[secure]==1.26.8
|
||||
urllib3[secure]==1.26.15
|
||||
# via
|
||||
# botocore
|
||||
# requests
|
||||
# scout-apm
|
||||
# sentry-sdk
|
||||
wagtail==4.1.3
|
||||
urllib3-secure-extra==0.1.0
|
||||
# via urllib3
|
||||
wagtail==4.1.4
|
||||
# via
|
||||
# -r requirements.in
|
||||
# wagtail-color-panel
|
||||
|
@ -267,9 +265,7 @@ whitenoise==6.4.0
|
|||
willow==1.4.1
|
||||
# via wagtail
|
||||
wrapt==1.13.3
|
||||
# via
|
||||
# deprecated
|
||||
# scout-apm
|
||||
# via scout-apm
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# setuptools
|
||||
|
|
39
tasks.py
39
tasks.py
|
@ -248,6 +248,17 @@ def manage(ctx, command, stop=False):
|
|||
pyrun(ctx, command, stop=stop)
|
||||
|
||||
|
||||
@task(aliases=["docker-djcheck"])
|
||||
def djcheck(ctx, stop=False):
|
||||
"""
|
||||
Django system check framework.
|
||||
|
||||
To stop the containers after the command has run, pass the `--stop` flag.
|
||||
"""
|
||||
print("Running system check framework...")
|
||||
manage(ctx, "check", stop=stop)
|
||||
|
||||
|
||||
@task(aliases=["docker-migrate"])
|
||||
def migrate(ctx, stop=False):
|
||||
"""
|
||||
|
@ -282,9 +293,31 @@ def test(ctx):
|
|||
|
||||
|
||||
@task(aliases=["docker-test-python"])
|
||||
def test_python(ctx):
|
||||
"""Run python tests."""
|
||||
manage(ctx, "test networkapi")
|
||||
def test_python(ctx, file="", n="auto", verbose=False):
|
||||
"""
|
||||
Run python tests.
|
||||
|
||||
Example calls:
|
||||
- test_python(ctx)
|
||||
- test_python(ctx, file="test_something.py")
|
||||
- test_python(ctx, n=4, verbose=True)
|
||||
|
||||
Parameters:
|
||||
- ctx: Context object (provided by Invoke)
|
||||
- file: Optional string representing the path to a specific test file to run.
|
||||
- n: Optional integer or string 'auto' representing the number of parallel tests to run.
|
||||
Default is 'auto' which allows pytest to automatically determine the optimal number.
|
||||
- verbose: Optional boolean flag indicating whether to print verbose output during testing. Default is False.
|
||||
"""
|
||||
|
||||
djcheck(ctx)
|
||||
makemigrations_dryrun(ctx, args="--check")
|
||||
parallel = f"-n {n}" if n != "1" else ""
|
||||
v = "-v" if verbose else ""
|
||||
# Don't run coverage if a file is specified
|
||||
cov = "" if file else "--cov=network-api/networkapi --cov-report=term-missing"
|
||||
command = f"pytest {v} {parallel} {file} --reuse-db {cov}"
|
||||
pyrun(ctx, command)
|
||||
|
||||
|
||||
# Linting
|
||||
|
|
Загрузка…
Ссылка в новой задаче