Merge branch 'main' into playwright-improvements

This commit is contained in:
Mavis Ou 2023-05-01 10:45:17 -07:00 коммит произвёл GitHub
Родитель ec686482f3 5ded577d8c
Коммит 3b40240e98
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
43 изменённых файлов: 7219 добавлений и 141 удалений

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

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

4
.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

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

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

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

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