From 345c5412671bcdf031d4bbc2a68b044b616b07c2 Mon Sep 17 00:00:00 2001 From: Dan Callahan Date: Mon, 10 Feb 2020 14:49:55 +0000 Subject: [PATCH] Use Black to format Python code (#6447) * Format Python code with Black * Update .flake8 for Black * Address most flake8 issues * Fix flake8 E231: missing whitespace after ',' Should be removed by Black, but currently buggy. See: https://github.com/psf/black/issues/1202 * Re-run Black * One final black/flake8 fix * Fail CI if Black does not pass --- .flake8 | 24 +- .travis.yml | 2 +- docs/conf.py | 87 +- kuma/__init__.py | 4 +- kuma/api/apps.py | 5 +- kuma/api/conftest.py | 44 +- kuma/api/management/commands/publish.py | 104 +- kuma/api/management/commands/unpublish.py | 27 +- kuma/api/signal_handlers.py | 15 +- kuma/api/tasks.py | 84 +- kuma/api/tests/test_signal_handlers.py | 20 +- kuma/api/tests/test_tasks.py | 343 +- kuma/api/urls.py | 2 +- kuma/api/v1/serializers.py | 15 +- kuma/api/v1/tests/test_views.py | 395 +- kuma/api/v1/urls.py | 20 +- kuma/api/v1/views.py | 200 +- kuma/attachments/admin.py | 120 +- kuma/attachments/apps.py | 5 +- kuma/attachments/feeds.py | 10 +- kuma/attachments/forms.py | 34 +- .../commands/empty_attachments_trash.py | 46 +- ...0001_squashed_0008_attachment_on_delete.py | 215 +- .../migrations/0002_auto_20191023_0405.py | 47 +- .../migrations/0003_auto_20191016_storage.py | 23 +- kuma/attachments/models.py | 111 +- kuma/attachments/signal_handlers.py | 8 +- kuma/attachments/tests/__init__.py | 4 +- kuma/attachments/tests/conftest.py | 15 +- kuma/attachments/tests/test_models.py | 80 +- kuma/attachments/tests/test_templates.py | 41 +- kuma/attachments/tests/test_views.py | 297 +- kuma/attachments/urls.py | 12 +- kuma/attachments/utils.py | 22 +- kuma/attachments/views.py | 45 +- kuma/authkeys/admin.py | 37 +- kuma/authkeys/decorators.py | 9 +- kuma/authkeys/forms.py | 2 +- kuma/authkeys/migrations/0001_initial.py | 91 +- kuma/authkeys/models.py | 46 +- kuma/authkeys/tests/conftest.py | 6 +- kuma/authkeys/tests/test_decorators.py | 22 +- kuma/authkeys/tests/test_views.py | 107 +- kuma/authkeys/urls.py | 12 +- kuma/authkeys/views.py | 32 +- kuma/banners/admin.py | 37 +- kuma/banners/migrations/0001_initial.py | 55 +- .../migrations/0002_auto_20191023_0405.py | 17 +- kuma/banners/models.py | 33 +- kuma/banners/tests/test_models.py | 10 +- kuma/celery.py | 11 +- kuma/conftest.py | 81 +- kuma/core/admin.py | 12 +- kuma/core/apps.py | 13 +- kuma/core/backends.py | 2 - kuma/core/context_processors.py | 40 +- kuma/core/decorators.py | 91 +- kuma/core/email_utils.py | 40 +- kuma/core/exceptions.py | 1 + kuma/core/form_fields.py | 32 +- kuma/core/ga_tracking.py | 40 +- kuma/core/i18n.py | 37 +- kuma/core/jobs.py | 36 +- kuma/core/management/commands/anonymize.py | 6 +- .../management/commands/delete_old_ip_bans.py | 8 +- kuma/core/management/commands/ihavepower.py | 22 +- .../commands/translate_locales_name.py | 31 +- kuma/core/managers.py | 19 +- kuma/core/middleware.py | 98 +- .../0001_squashed_0004_remove_unused_tags.py | 27 +- .../migrations/0002_auto_20191206_0805.py | 14 +- .../migrations/0003_auto_20191212_0638.py | 12 +- kuma/core/models.py | 5 +- kuma/core/pipeline/cleancss.py | 6 +- kuma/core/pipeline/sass.py | 2 - kuma/core/pipeline/storage.py | 2 - kuma/core/tasks.py | 28 +- kuma/core/templatetags/jinja_helpers.py | 54 +- kuma/core/tests/__init__.py | 26 +- kuma/core/tests/logging_urls.py | 10 +- kuma/core/tests/taggit_extras/models.py | 2 - kuma/core/tests/test_backends.py | 20 +- kuma/core/tests/test_commands.py | 10 +- kuma/core/tests/test_decorators.py | 145 +- kuma/core/tests/test_form_fields.py | 21 +- kuma/core/tests/test_ga_tracking.py | 26 +- kuma/core/tests/test_helpers.py | 180 +- kuma/core/tests/test_jobs.py | 60 +- kuma/core/tests/test_locale_middleware.py | 149 +- kuma/core/tests/test_middleware.py | 77 +- kuma/core/tests/test_misc.py | 11 +- kuma/core/tests/test_models.py | 12 +- kuma/core/tests/test_pagination.py | 25 +- kuma/core/tests/test_settings.py | 13 +- kuma/core/tests/test_taggit_extras.py | 48 +- kuma/core/tests/test_templates.py | 44 +- kuma/core/tests/test_utils.py | 68 +- kuma/core/tests/test_validators.py | 52 +- kuma/core/tests/test_views.py | 174 +- kuma/core/urlresolvers.py | 57 +- kuma/core/utils.py | 151 +- kuma/core/validators.py | 116 +- kuma/core/views.py | 33 +- kuma/dashboards/forms.py | 59 +- kuma/dashboards/jobs.py | 16 +- kuma/dashboards/tests/test_views.py | 636 +-- kuma/dashboards/urls.py | 33 +- kuma/dashboards/utils.py | 209 +- kuma/dashboards/views.py | 148 +- kuma/feeder/admin.py | 2 - kuma/feeder/apps.py | 13 +- .../management/commands/update_feeds.py | 24 +- kuma/feeder/migrations/0001_initial.py | 132 +- .../migrations/0002_auto_20191023_0405.py | 51 +- kuma/feeder/models.py | 40 +- kuma/feeder/sections.py | 8 +- kuma/feeder/tasks.py | 2 - kuma/feeder/tests/test_models.py | 42 +- kuma/feeder/tests/test_tasks.py | 4 +- kuma/feeder/tests/test_utils.py | 148 +- kuma/feeder/utils.py | 77 +- kuma/health/tests/test_views.py | 311 +- kuma/health/urls.py | 20 +- kuma/health/views.py | 139 +- kuma/landing/tests/test_templates.py | 24 +- kuma/landing/tests/test_utils.py | 16 +- kuma/landing/tests/test_views.py | 82 +- kuma/landing/urls.py | 32 +- kuma/landing/utils.py | 10 +- kuma/landing/views.py | 33 +- kuma/payments/apps.py | 4 +- kuma/payments/constants.py | 6 +- kuma/payments/tests/test_utils.py | 62 +- kuma/payments/tests/test_views.py | 204 +- kuma/payments/urls.py | 22 +- kuma/payments/utils.py | 41 +- kuma/payments/views.py | 51 +- kuma/redirects/redirects.py | 1792 +++++---- kuma/scrape/fixture.py | 246 +- kuma/scrape/management/commands/__init__.py | 19 +- kuma/scrape/management/commands/sample_mdn.py | 62 +- .../management/commands/scrape_document.py | 64 +- .../management/commands/scrape_links.py | 57 +- .../scrape/management/commands/scrape_user.py | 45 +- kuma/scrape/scraper.py | 150 +- kuma/scrape/sources/base.py | 85 +- kuma/scrape/sources/document.py | 134 +- kuma/scrape/sources/document_children.py | 24 +- kuma/scrape/sources/document_current.py | 40 +- kuma/scrape/sources/document_history.py | 33 +- kuma/scrape/sources/document_meta.py | 23 +- kuma/scrape/sources/document_redirect.py | 12 +- kuma/scrape/sources/links.py | 44 +- kuma/scrape/sources/revision.py | 104 +- kuma/scrape/sources/user.py | 84 +- kuma/scrape/storage.py | 108 +- kuma/scrape/tests/__init__.py | 32 +- kuma/scrape/tests/conftest.py | 37 +- kuma/scrape/tests/test_fixture.py | 126 +- kuma/scrape/tests/test_scraper.py | 159 +- kuma/scrape/tests/test_source.py | 180 +- kuma/scrape/tests/test_source_document.py | 356 +- .../scrape/tests/test_source_document_base.py | 22 +- .../tests/test_source_document_children.py | 86 +- .../tests/test_source_document_current.py | 84 +- .../tests/test_source_document_history.py | 112 +- .../scrape/tests/test_source_document_meta.py | 91 +- .../tests/test_source_document_redirect.py | 29 +- kuma/scrape/tests/test_source_links.py | 48 +- kuma/scrape/tests/test_source_revision.py | 336 +- kuma/scrape/tests/test_source_user.py | 108 +- kuma/scrape/tests/test_storage.py | 350 +- kuma/search/admin.py | 50 +- kuma/search/apps.py | 13 +- kuma/search/decorators.py | 12 +- kuma/search/fields.py | 14 +- kuma/search/filters.py | 104 +- kuma/search/forms.py | 11 +- kuma/search/jobs.py | 4 +- .../commands/generate_search_names.py | 8 +- kuma/search/management/commands/reindex.py | 31 +- kuma/search/managers.py | 20 +- .../0001_squashed_0003_filter_tags.py | 223 +- .../0002_auto_20191002_filter_base_manager.py | 5 +- .../migrations/0003_auto_20191023_0405.py | 124 +- kuma/search/models.py | 166 +- kuma/search/names.py | 64 +- kuma/search/pagination.py | 41 +- kuma/search/paginator.py | 17 +- kuma/search/queries.py | 23 +- kuma/search/renderers.py | 16 +- kuma/search/search.py | 69 +- kuma/search/serializers.py | 28 +- kuma/search/signal_handlers.py | 29 +- kuma/search/store.py | 21 +- kuma/search/tasks.py | 21 +- kuma/search/tests/__init__.py | 6 +- kuma/search/tests/test_filters.py | 552 +-- kuma/search/tests/test_indexes.py | 32 +- kuma/search/tests/test_serializers.py | 91 +- kuma/search/tests/test_store.py | 53 +- kuma/search/tests/test_tasks.py | 1 - kuma/search/tests/test_types.py | 30 +- kuma/search/tests/test_utils.py | 52 +- kuma/search/tests/test_views.py | 161 +- kuma/search/tests/test_views_admin.py | 11 +- kuma/search/urls.py | 8 +- kuma/search/utils.py | 18 +- kuma/search/views.py | 32 +- kuma/settings/common.py | 2181 +++++------ kuma/settings/local.py | 37 +- kuma/settings/prod.py | 32 +- kuma/settings/testing.py | 60 +- kuma/spam/akismet.py | 66 +- kuma/spam/constants.py | 18 +- kuma/spam/forms.py | 29 +- kuma/spam/models.py | 35 +- kuma/spam/tests/test_akismet.py | 159 +- kuma/spam/tests/test_forms.py | 81 +- kuma/urls.py | 144 +- kuma/urls_untrusted.py | 13 +- kuma/users/adapters.py | 148 +- kuma/users/admin.py | 42 +- kuma/users/apps.py | 5 +- kuma/users/auth_backends.py | 6 +- kuma/users/constants.py | 9 +- kuma/users/forms.py | 215 +- .../commands/configure_social_auth.py | 77 +- ...01_squashed_0008_update_locales_tz_help.py | 3454 ++++++++++++++++- .../0002_pytz_2018_5_and_username.py | 528 ++- .../migrations/0003_user_discourse_url.py | 18 +- .../0004_add_choices_to_timezone.py | 515 ++- .../migrations/0005_stripe_customer_id.py | 6 +- kuma/users/migrations/0006_pytz_2018_9.py | 516 ++- .../migrations/0007_auto_20190509_1229.py | 516 ++- .../migrations/0008_auto_20190610_0822.py | 13 +- .../0009_merge_bn_bd_and_bn_in_to_bn.py | 12 +- .../migrations/0010_auto_20190912_1634.py | 87 +- .../migrations/0011_auto_20190914_2208.py | 59 +- .../migrations/0012_auto_20190914_2209.py | 37 +- .../migrations/0013_auto_20191023_0741.py | 238 +- .../migrations/0014_auto_20200110_0519.py | 12 +- .../0014_user_is_newsletter_subscribed.py | 6 +- kuma/users/models.py | 194 +- kuma/users/providers/github/provider.py | 32 +- kuma/users/providers/github/views.py | 30 +- kuma/users/providers/google/provider.py | 10 +- kuma/users/providers/google/views.py | 15 +- kuma/users/signal_handlers.py | 60 +- kuma/users/signup.py | 57 +- kuma/users/tasks.py | 31 +- kuma/users/templatetags/jinja_helpers.py | 122 +- kuma/users/tests/__init__.py | 229 +- kuma/users/tests/test_adapters.py | 379 +- kuma/users/tests/test_auth_backends.py | 9 +- kuma/users/tests/test_forms.py | 118 +- kuma/users/tests/test_helpers.py | 60 +- kuma/users/tests/test_models.py | 82 +- kuma/users/tests/test_signal_handlers.py | 34 +- kuma/users/tests/test_stripe_subscription.py | 44 +- kuma/users/tests/test_tasks.py | 87 +- kuma/users/tests/test_templates.py | 1054 +++-- kuma/users/tests/test_views.py | 1235 +++--- kuma/users/urls.py | 142 +- kuma/users/utils.py | 39 +- kuma/users/views.py | 537 +-- kuma/version/tests/test_views.py | 40 +- kuma/version/urls.py | 10 +- kuma/version/views.py | 5 +- kuma/wiki/admin.py | 454 ++- kuma/wiki/apps.py | 13 +- kuma/wiki/constants.py | 840 ++-- kuma/wiki/content.py | 560 +-- kuma/wiki/decorators.py | 43 +- kuma/wiki/events.py | 128 +- kuma/wiki/exceptions.py | 1 + kuma/wiki/feeds.py | 318 +- kuma/wiki/forms.py | 613 +-- kuma/wiki/jobs.py | 31 +- kuma/wiki/kumascript.py | 103 +- .../management/commands/clean_document.py | 67 +- .../correct_current_revision_documents.py | 132 +- .../commands/delete_old_revision_ips.py | 8 +- .../commands/delete_old_spam_attempt_data.py | 8 +- .../wiki/management/commands/make_sitemaps.py | 13 +- .../commands/populate_attachments.py | 94 +- .../commands/refresh_wiki_caches.py | 20 +- .../management/commands/render_document.py | 175 +- .../repair_translation_breadcrumbs.py | 28 +- .../commands/submit_deleted_documents.py | 92 +- kuma/wiki/managers.py | 76 +- kuma/wiki/middleware.py | 7 +- .../0001_squashed_0036_update_locales.py | 1539 ++++++-- .../migrations/0002_remove_document_zone.py | 16 +- kuma/wiki/migrations/0003_bcsignal.py | 32 +- .../migrations/0004_auto_20190708_1314.py | 6 +- .../migrations/0005_auto_20190805_0306.py | 26 +- .../0005_change_locale_bn_bd_to_bn.py | 14 +- .../migrations/0006_auto_20190912_1634.py | 168 +- .../migrations/0006_auto_20191023_0741.py | 182 +- .../migrations/0007_auto_20190914_2208.py | 112 +- .../migrations/0008_auto_20190914_2209.py | 37 +- kuma/wiki/models.py | 731 ++-- kuma/wiki/search.py | 219 +- kuma/wiki/signal_handlers.py | 16 +- kuma/wiki/signals.py | 5 +- kuma/wiki/tasks.py | 270 +- kuma/wiki/templatetags/jinja_helpers.py | 119 +- kuma/wiki/templatetags/ssr.py | 40 +- kuma/wiki/tests/__init__.py | 97 +- kuma/wiki/tests/conftest.py | 329 +- kuma/wiki/tests/test_admin.py | 319 +- kuma/wiki/tests/test_content.py | 781 ++-- kuma/wiki/tests/test_events.py | 249 +- kuma/wiki/tests/test_feeds.py | 265 +- kuma/wiki/tests/test_forms.py | 811 ++-- kuma/wiki/tests/test_helpers.py | 117 +- kuma/wiki/tests/test_jobs.py | 21 +- kuma/wiki/tests/test_kumascript.py | 269 +- kuma/wiki/tests/test_managers.py | 146 +- kuma/wiki/tests/test_models.py | 1252 +++--- kuma/wiki/tests/test_models_document.py | 248 +- kuma/wiki/tests/test_search.py | 21 +- kuma/wiki/tests/test_signal_handlers.py | 20 +- kuma/wiki/tests/test_ssr.py | 119 +- kuma/wiki/tests/test_tasks.py | 141 +- kuma/wiki/tests/test_templates.py | 1100 +++--- kuma/wiki/tests/test_utils.py | 139 +- kuma/wiki/tests/test_views.py | 2703 +++++++------ kuma/wiki/tests/test_views_admin.py | 40 +- kuma/wiki/tests/test_views_akismet.py | 105 +- kuma/wiki/tests/test_views_code.py | 131 +- kuma/wiki/tests/test_views_create.py | 254 +- kuma/wiki/tests/test_views_delete.py | 102 +- kuma/wiki/tests/test_views_document.py | 718 ++-- kuma/wiki/tests/test_views_edit.py | 14 +- kuma/wiki/tests/test_views_list.py | 207 +- kuma/wiki/tests/test_views_misc.py | 76 +- kuma/wiki/tests/test_views_revision.py | 245 +- kuma/wiki/tests/test_views_translate.py | 44 +- kuma/wiki/urls.py | 250 +- kuma/wiki/urls_untrusted.py | 19 +- kuma/wiki/utils.py | 87 +- kuma/wiki/views/__init__.py | 19 +- kuma/wiki/views/akismet_revision.py | 38 +- kuma/wiki/views/code.py | 19 +- kuma/wiki/views/create.py | 95 +- kuma/wiki/views/delete.py | 112 +- kuma/wiki/views/document.py | 637 +-- kuma/wiki/views/edit.py | 211 +- kuma/wiki/views/legacy.py | 42 +- kuma/wiki/views/list.py | 166 +- kuma/wiki/views/misc.py | 58 +- kuma/wiki/views/revision.py | 169 +- kuma/wiki/views/translate.py | 197 +- kuma/wiki/views/utils.py | 36 +- kuma/wsgi.py | 2 +- tests/conftest.py | 66 +- tests/headless/__init__.py | 15 +- tests/headless/map_301.py | 1417 ++++--- tests/headless/test_cdn.py | 198 +- tests/headless/test_endpoints.py | 433 ++- tests/headless/test_redirects.py | 67 +- tests/headless/test_robots.py | 10 +- tests/utils/urls.py | 68 +- 365 files changed, 31126 insertions(+), 21259 deletions(-) diff --git a/.flake8 b/.flake8 index ee5dc860591..ded63298622 100644 --- a/.flake8 +++ b/.flake8 @@ -1,11 +1,19 @@ [flake8] -exclude = **/migrations/**,.tox,*.egg,vendor -# E501 - line too long (82 > 79 characters) -# E731 - do not assign a lambda expression, use a def -# F405 - name may be undefined, or defined from star imports: module -# W504 - line break after binary operator -# conflicts with W503 (line break before binary operator) -ignore = E501,E731,F405,W504 +exclude = **/migrations/** + +# Black recommends 88-char lines and ignoring the following lints: +# - E203 - whitespace before ':' +# - E501 - line too long +# - W503 - line break before binary operator +max-line-length=88 +ignore = E203, E501, W503 + +# Allow star imports in config files: +per-file-ignores = + kuma/settings/local.py:F403,F405 + kuma/settings/prod.py:F403,F405 + kuma/settings/testing.py:F403,F405 + # flake8-import-order settings import-order-style=edited -application-import-names=kuma,pages,utils \ No newline at end of file +application-import-names=kuma,pages,utils diff --git a/.travis.yml b/.travis.yml index 5ad21f6465a..7e66d057997 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,7 +26,7 @@ matrix: - poetry --version - poetry install -vv - poetry run flake8 kuma docs tests - - poetry run black --check kuma docs tests || echo "We're not letting it break things yet" + - poetry run black --check --diff kuma docs tests - language: python name: "Python testing" python: 3.8 diff --git a/docs/conf.py b/docs/conf.py index 67f0aa1b81d..9cc76ddec52 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,29 +27,29 @@ extensions = [] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'Kuma' -copyright = 'Mozilla' +project = "Kuma" +copyright = "Mozilla" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = 'latest' +version = "latest" # The full version, including alpha/beta/rc tags. -release = 'latest' +release = "latest" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -64,7 +64,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None @@ -94,7 +94,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'alabaster' +html_theme = "alabaster" # Add any paths that contain custom themes here, relative to this directory. html_theme_path = ["."] @@ -103,26 +103,28 @@ # further. For a list of options available for each theme, see the # documentation. html_theme_options = { - 'github_user': 'mozilla', - 'github_repo': 'kuma', - 'github_button': False, - 'description': ('The platform that powers ' - 'MDN'), - 'travis_button': False, - 'codecov_button': False, - 'extra_nav_links': { - 'MDN': 'https://developer.mozilla.org', - 'MDN Staging': 'https://developer.allizom.org', - 'Kuma on GitHub': 'https://github.com/mdn/kuma', - 'KumaScript on GitHub': 'https://github.com/mdn/kumascript', + "github_user": "mozilla", + "github_repo": "kuma", + "github_button": False, + "description": ( + "The platform that powers " + 'MDN' + ), + "travis_button": False, + "codecov_button": False, + "extra_nav_links": { + "MDN": "https://developer.mozilla.org", + "MDN Staging": "https://developer.allizom.org", + "Kuma on GitHub": "https://github.com/mdn/kuma", + "KumaScript on GitHub": "https://github.com/mdn/kumascript", }, - 'show_related': True, - 'page_width': '100%' + "show_related": True, + "page_width": "100%", } # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -html_title = 'Kuma Documentation' +html_title = "Kuma Documentation" # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None @@ -151,12 +153,12 @@ # Custom sidebar templates, maps document names to template names. html_sidebars = { - '**': [ - 'about.html', - 'navigation.html', - 'relations.html', - 'searchbox.html', - 'donate.html', + "**": [ + "about.html", + "navigation.html", + "relations.html", + "searchbox.html", + "donate.html", ] } @@ -191,7 +193,7 @@ # html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'Kumadoc' +htmlhelp_basename = "Kumadoc" # -- Options for LaTeX output -------------------------------------------------- @@ -199,10 +201,8 @@ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # 'preamble': '', } @@ -210,8 +210,7 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'Kuma.tex', 'Kuma Documentation', - 'Mozilla', 'manual'), + ("index", "Kuma.tex", "Kuma Documentation", "Mozilla", "manual"), ] # The name of an image file (relative to this directory) to place at the top of @@ -239,10 +238,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'kuma', 'Kuma Documentation', - ['Mozilla'], 1) -] +man_pages = [("index", "kuma", "Kuma Documentation", ["Mozilla"], 1)] # If true, show URL addresses after external links. # man_show_urls = False @@ -254,10 +250,15 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'Kuma', 'Kuma Documentation', - 'Mozilla', 'Kuma', - 'The Django based project of developer.mozilla.org.', - 'Miscellaneous'), + ( + "index", + "Kuma", + "Kuma Documentation", + "Mozilla", + "Kuma", + "The Django based project of developer.mozilla.org.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. diff --git a/kuma/__init__.py b/kuma/__init__.py index 8e4110a6904..5568b6d791f 100644 --- a/kuma/__init__.py +++ b/kuma/__init__.py @@ -1,7 +1,5 @@ - - # This will make sure the app is always imported when # Django starts so that shared_task will use this app. from .celery import app as celery_app -__all__ = ('celery_app',) +__all__ = ("celery_app",) diff --git a/kuma/api/apps.py b/kuma/api/apps.py index fdb9bc23515..e33aa5d23df 100644 --- a/kuma/api/apps.py +++ b/kuma/api/apps.py @@ -6,8 +6,9 @@ class APIConfig(AppConfig): The Django App Config class to store information about the API app and do startup time things. """ - name = 'kuma.api' - verbose_name = 'API' + + name = "kuma.api" + verbose_name = "API" def ready(self): """Configure kuma.api after models are loaded.""" diff --git a/kuma/api/conftest.py b/kuma/api/conftest.py index 1bb2fbe31e7..b099909e3ea 100644 --- a/kuma/api/conftest.py +++ b/kuma/api/conftest.py @@ -12,18 +12,18 @@ def redirect_to_self(wiki_user): """ A top-level English document that redirects to itself. """ - doc = Document.objects.create( - locale='en-US', slug='GoMe', title='Redirect to Self') + doc = Document.objects.create(locale="en-US", slug="GoMe", title="Redirect to Self") Revision.objects.create( document=doc, creator=wiki_user, - content=REDIRECT_CONTENT % { - 'href': reverse('wiki.document', locale=doc.locale, - args=(doc.slug,)), - 'title': doc.title, + content=REDIRECT_CONTENT + % { + "href": reverse("wiki.document", locale=doc.locale, args=(doc.slug,)), + "title": doc.title, }, - title='Redirect to Self', - created=datetime(2018, 9, 16, 11, 15)) + title="Redirect to Self", + created=datetime(2018, 9, 16, 11, 15), + ) return doc @@ -33,16 +33,15 @@ def redirect_to_home(wiki_user): A top-level English document that redirects to the home page. """ doc = Document.objects.create( - locale='en-US', slug='GoHome', title='Redirect to Home Page') + locale="en-US", slug="GoHome", title="Redirect to Home Page" + ) Revision.objects.create( document=doc, creator=wiki_user, - content=REDIRECT_CONTENT % { - 'href': reverse('home'), - 'title': 'MDN Web Docs', - }, - title='Redirect to Home Page', - created=datetime(2015, 7, 4, 11, 15)) + content=REDIRECT_CONTENT % {"href": reverse("home"), "title": "MDN Web Docs"}, + title="Redirect to Home Page", + created=datetime(2015, 7, 4, 11, 15), + ) return doc @@ -52,14 +51,17 @@ def redirect_to_macros(wiki_user): A top-level English document that redirects to the macros dashboard. """ doc = Document.objects.create( - locale='en-US', slug='GoMacros', title='Redirect to Macros Dashboard') + locale="en-US", slug="GoMacros", title="Redirect to Macros Dashboard" + ) Revision.objects.create( document=doc, creator=wiki_user, - content=REDIRECT_CONTENT % { - 'href': reverse('dashboards.macros', locale='en-US'), - 'title': 'Active macros | MDN', + content=REDIRECT_CONTENT + % { + "href": reverse("dashboards.macros", locale="en-US"), + "title": "Active macros | MDN", }, - title='Redirect to Macros Dashboard', - created=datetime(2017, 5, 24, 12, 15)) + title="Redirect to Macros Dashboard", + created=datetime(2017, 5, 24, 12, 15), + ) return doc diff --git a/kuma/api/management/commands/publish.py b/kuma/api/management/commands/publish.py index 964268d259d..072be8135ef 100644 --- a/kuma/api/management/commands/publish.py +++ b/kuma/api/management/commands/publish.py @@ -15,97 +15,105 @@ class Command(BaseCommand): - args = '' - help = 'Publish one or more documents to the document API' + args = "" + help = "Publish one or more documents to the document API" def add_arguments(self, parser): parser.add_argument( - 'paths', - help='Path to document(s), like /en-US/docs/Web', - nargs='*', # overridden by --all or --locale - metavar='path') + "paths", + help="Path to document(s), like /en-US/docs/Web", + nargs="*", # overridden by --all or --locale + metavar="path", + ) parser.add_argument( - '--all', - help='Publish ALL documents (rather than by path)', - action='store_true') + "--all", + help="Publish ALL documents (rather than by path)", + action="store_true", + ) parser.add_argument( - '--locale', - help='Publish ALL documents in this locale (rather than by path)') + "--locale", + help="Publish ALL documents in this locale (rather than by path)", + ) parser.add_argument( - '--chunk-size', + "--chunk-size", type=int, default=1000, - help='Partition the work into tasks, with each task handling this ' - 'many documents (default=1000)') + help="Partition the work into tasks, with each task handling this " + "many documents (default=1000)", + ) parser.add_argument( - '--skip-cdn-invalidation', + "--skip-cdn-invalidation", help=( - 'No CDN cache invalidation after publishing. Forced to True ' - 'if either the --all or --locale flag is used.' + "No CDN cache invalidation after publishing. Forced to True " + "if either the --all or --locale flag is used." ), - action='store_true') + action="store_true", + ) def handle(self, *args, **options): - Logger = namedtuple('Logger', 'info, error') + Logger = namedtuple("Logger", "info, error") log = Logger(info=self.stdout.write, error=self.stderr.write) - if options['all'] or options['locale']: - if options['locale'] and options['all']: + if options["all"] or options["locale"]: + if options["locale"] and options["all"]: raise CommandError( - 'Specifying --locale with --all is the same as --all' + "Specifying --locale with --all is the same as --all" ) filters = {} - if options['locale']: - locale = options['locale'] - log.info('Publishing all documents in locale {}'.format(locale)) + if options["locale"]: + locale = options["locale"] + log.info("Publishing all documents in locale {}".format(locale)) filters.update(locale=locale) else: - log.info('Publishing all documents') - chunk_size = max(options['chunk_size'], 1) + log.info("Publishing all documents") + chunk_size = max(options["chunk_size"], 1) docs = Document.objects.filter(**filters) - doc_pks = docs.values_list('id', flat=True) + doc_pks = docs.values_list("id", flat=True) num_docs = len(doc_pks) num_tasks = int(ceil(num_docs / float(chunk_size))) - log.info('...found {} documents.'.format(num_docs)) + log.info("...found {} documents.".format(num_docs)) # Let's publish the documents in a group of chunks, where the # tasks in the group can be run in parallel. tasks = [] for i, chunk in enumerate(chunked(doc_pks, chunk_size)): - message = 'Published chunk #{} of {}'.format(i + 1, num_tasks) - tasks.append(publish.si( - chunk, - completion_message=message, - invalidate_cdn_cache=False - )) + message = "Published chunk #{} of {}".format(i + 1, num_tasks) + tasks.append( + publish.si( + chunk, completion_message=message, invalidate_cdn_cache=False + ) + ) if num_tasks == 1: - msg = ('Launching a single task handling ' - 'all {} documents.'.format(num_docs)) + msg = "Launching a single task handling " "all {} documents.".format( + num_docs + ) else: - msg = ('Launching {} paralellizable tasks, each handling ' - 'at most {} documents.'.format(num_tasks, chunk_size)) + msg = ( + "Launching {} paralellizable tasks, each handling " + "at most {} documents.".format(num_tasks, chunk_size) + ) log.info(msg) group(*tasks).apply_async() else: - paths = options['paths'] + paths = options["paths"] if not paths: - raise CommandError('Need at least one document path to publish') + raise CommandError("Need at least one document path to publish") doc_pks = [] - get_doc_pk = Document.objects.values_list('id', flat=True).get + get_doc_pk = Document.objects.values_list("id", flat=True).get for path in paths: - if path.startswith('/'): + if path.startswith("/"): path = path[1:] - locale, sep, slug = path.partition('/') - head, sep, tail = slug.partition('/') - if head == 'docs': + locale, sep, slug = path.partition("/") + head, sep, tail = slug.partition("/") + if head == "docs": slug = tail try: doc_pk = get_doc_pk(locale=locale, slug=slug) except Document.DoesNotExist: - msg = 'Document with locale={} and slug={} does not exist' + msg = "Document with locale={} and slug={} does not exist" log.error(msg.format(locale, slug)) else: doc_pks.append(doc_pk) publish( doc_pks, log=log, - invalidate_cdn_cache=(not options['skip_cdn_invalidation']) + invalidate_cdn_cache=(not options["skip_cdn_invalidation"]), ) diff --git a/kuma/api/management/commands/unpublish.py b/kuma/api/management/commands/unpublish.py index 5d110fad320..3ef347e0e4b 100644 --- a/kuma/api/management/commands/unpublish.py +++ b/kuma/api/management/commands/unpublish.py @@ -11,29 +11,30 @@ class Command(BaseCommand): - args = '' - help = 'Remove one or more documents from the document API' + args = "" + help = "Remove one or more documents from the document API" def add_arguments(self, parser): parser.add_argument( - 'paths', - help='Path to document(s), like /en-US/docs/Web', - nargs='*', - metavar='path') + "paths", + help="Path to document(s), like /en-US/docs/Web", + nargs="*", + metavar="path", + ) def handle(self, *args, **options): - Logger = namedtuple('Logger', 'info, error') + Logger = namedtuple("Logger", "info, error") log = Logger(info=self.stdout.write, error=self.stderr.write) - paths = options['paths'] + paths = options["paths"] if not paths: - raise CommandError('Need at least one document path to remove') + raise CommandError("Need at least one document path to remove") doc_locale_slug_pairs = [] for path in paths: - if path.startswith('/'): + if path.startswith("/"): path = path[1:] - locale, sep, slug = path.partition('/') - head, sep, tail = slug.partition('/') - if head == 'docs': + locale, sep, slug = path.partition("/") + head, sep, tail = slug.partition("/") + if head == "docs": slug = tail doc_locale_slug_pairs.append((locale, slug)) unpublish(doc_locale_slug_pairs, log=log) diff --git a/kuma/api/signal_handlers.py b/kuma/api/signal_handlers.py index bfff01e0a00..8f2a3b54b4f 100644 --- a/kuma/api/signal_handlers.py +++ b/kuma/api/signal_handlers.py @@ -1,5 +1,3 @@ - - from django.db.models.signals import post_delete from django.dispatch import receiver @@ -9,8 +7,9 @@ from .tasks import publish, unpublish -@receiver(restore_done, sender=Document, - dispatch_uid='api.document.restore_done.publish') +@receiver( + restore_done, sender=Document, dispatch_uid="api.document.restore_done.publish" +) def on_restore_done(sender, instance, **kwargs): """ A signal handler to publish the document to the document API after it @@ -19,8 +18,7 @@ def on_restore_done(sender, instance, **kwargs): publish.delay([instance.pk]) -@receiver(render_done, sender=Document, - dispatch_uid='api.document.render_done.publish') +@receiver(render_done, sender=Document, dispatch_uid="api.document.render_done.publish") def on_render_done(sender, instance, invalidate_cdn_cache, **kwargs): """ A signal handler to publish the document to the document API after it @@ -30,8 +28,9 @@ def on_render_done(sender, instance, invalidate_cdn_cache, **kwargs): publish.delay([instance.pk], invalidate_cdn_cache=invalidate_cdn_cache) -@receiver(post_delete, sender=Document, - dispatch_uid='api.document.post_delete.unpublish') +@receiver( + post_delete, sender=Document, dispatch_uid="api.document.post_delete.unpublish" +) def on_post_delete(instance, **kwargs): """ A signal handler to remove the given document from the document API after diff --git a/kuma/api/tasks.py b/kuma/api/tasks.py index aac5245bd94..237b0622de6 100644 --- a/kuma/api/tasks.py +++ b/kuma/api/tasks.py @@ -1,5 +1,3 @@ - - import json import time @@ -28,7 +26,7 @@ def get_s3_resource(config=None): """ global _s3_resource if _s3_resource is None: - _s3_resource = boto3.resource('s3', config=config) + _s3_resource = boto3.resource("s3", config=config) return _s3_resource @@ -45,7 +43,7 @@ def get_cloudfront_client(config=None): """ global _cloudfront_client if _cloudfront_client is None: - _cloudfront_client = boto3.client('cloudfront', config=config) + _cloudfront_client = boto3.client("cloudfront", config=config) return _cloudfront_client @@ -61,8 +59,9 @@ def get_s3_bucket(config=None): @task -def unpublish(doc_locale_slug_pairs, log=None, completion_message=None, - invalidate_cdn_cache=True): +def unpublish( + doc_locale_slug_pairs, log=None, completion_message=None, invalidate_cdn_cache=True +): """ Delete one or more documents from the S3 bucket serving the document API. """ @@ -71,24 +70,29 @@ def unpublish(doc_locale_slug_pairs, log=None, completion_message=None, s3_bucket = get_s3_bucket() if not s3_bucket: - log.info('Skipping unpublish of {!r}: no S3 bucket configured'.format( - doc_locale_slug_pairs)) + log.info( + "Skipping unpublish of {!r}: no S3 bucket configured".format( + doc_locale_slug_pairs + ) + ) return - keys_to_delete = (get_s3_key(locale=locale, slug=slug) - for locale, slug in doc_locale_slug_pairs) + keys_to_delete = ( + get_s3_key(locale=locale, slug=slug) for locale, slug in doc_locale_slug_pairs + ) for chunk in chunked(keys_to_delete, S3_MAX_KEYS_PER_DELETE): response = s3_bucket.delete_objects( - Delete={ - 'Objects': [{'Key': key} for key in chunk] - } + Delete={"Objects": [{"Key": key} for key in chunk]} ) - for info in response.get('Deleted', ()): - log.info('Unpublished {}'.format(info['Key'])) - for info in response.get('Errors', ()): - log.error('Unable to unpublish {}: ({}) {}'.format( - info['Key'], info['Code'], info['Message'])) + for info in response.get("Deleted", ()): + log.info("Unpublished {}".format(info["Key"])) + for info in response.get("Errors", ()): + log.error( + "Unable to unpublish {}: ({}) {}".format( + info["Key"], info["Code"], info["Message"] + ) + ) if completion_message: log.info(completion_message) @@ -98,8 +102,7 @@ def unpublish(doc_locale_slug_pairs, log=None, completion_message=None, @task -def publish(doc_pks, log=None, completion_message=None, - invalidate_cdn_cache=True): +def publish(doc_pks, log=None, completion_message=None, invalidate_cdn_cache=True): """ Publish one or more documents to the S3 bucket serving the document API. """ @@ -108,8 +111,7 @@ def publish(doc_pks, log=None, completion_message=None, s3_bucket = get_s3_bucket() if not s3_bucket: - log.info( - 'Skipping publish of {!r}: no S3 bucket configured'.format(doc_pks)) + log.info("Skipping publish of {!r}: no S3 bucket configured".format(doc_pks)) return if invalidate_cdn_cache: @@ -120,7 +122,7 @@ def publish(doc_pks, log=None, completion_message=None, try: doc = Document.objects.get(pk=pk) except Document.DoesNotExist: - log.error('Document with pk={} does not exist'.format(pk)) + log.error("Document with pk={} does not exist".format(pk)) continue if invalidate_cdn_cache: @@ -129,9 +131,9 @@ def publish(doc_pks, log=None, completion_message=None, doc_locale_slug_pairs.append((doc.locale, doc.slug)) kwargs = dict( - ACL='public-read', + ACL="public-read", Key=get_s3_key(doc), - ContentType='application/json', + ContentType="application/json", ContentLanguage=doc.locale, ) redirect = get_content_based_redirect(doc) @@ -145,7 +147,7 @@ def publish(doc_pks, log=None, completion_message=None, data = document_api_data(doc) kwargs.update(Body=json.dumps(data)) s3_object = s3_bucket.put_object(**kwargs) - log.info('Published {!r}'.format(s3_object)) + log.info("Published {!r}".format(s3_object)) if completion_message: log.info(completion_message) @@ -166,27 +168,21 @@ def request_cdn_cache_invalidation(doc_locale_slug_pairs, log=None): client = get_cloudfront_client() for label, conf in settings.MDN_CLOUDFRONT_DISTRIBUTIONS.items(): - if not conf['id']: - log.info('No Distribution ID available for CloudFront {!r}'.format( - label - )) + if not conf["id"]: + log.info("No Distribution ID available for CloudFront {!r}".format(label)) continue - transform_function = import_string(conf['transform_function']) + transform_function = import_string(conf["transform_function"]) paths = ( - transform_function(locale, slug) - for locale, slug in doc_locale_slug_pairs + transform_function(locale, slug) for locale, slug in doc_locale_slug_pairs ) # In case the transform function decided to "opt-out" on a particular # (locale, slug) it might return a falsy value. paths = [x for x in paths if x] if paths: invalidation = client.create_invalidation( - DistributionId=conf['id'], + DistributionId=conf["id"], InvalidationBatch={ - 'Paths': { - 'Quantity': len(paths), - 'Items': paths - }, + "Paths": {"Quantity": len(paths), "Items": paths}, # The 'CallerReference' just needs to be a unique string. # By using a timestamp we get slightly more information # than using a UUID or a random string. But it needs to @@ -194,14 +190,12 @@ def request_cdn_cache_invalidation(doc_locale_slug_pairs, log=None): # significant figures to avoid the unlikely chance that # this code gets executed concurrently within a small # time window. - 'CallerReference': '{:.6f}'.format(time.time()) - } + "CallerReference": "{:.6f}".format(time.time()), + }, ) log.info( - 'Issued cache invalidation for {!r} in {} distribution' - ' (received with {})'.format( - paths, - label, - invalidation['ResponseMetadata']['HTTPStatusCode'] + "Issued cache invalidation for {!r} in {} distribution" + " (received with {})".format( + paths, label, invalidation["ResponseMetadata"]["HTTPStatusCode"] ) ) diff --git a/kuma/api/tests/test_signal_handlers.py b/kuma/api/tests/test_signal_handlers.py index 33eafb8f52f..150bb1e51ba 100644 --- a/kuma/api/tests/test_signal_handlers.py +++ b/kuma/api/tests/test_signal_handlers.py @@ -6,27 +6,29 @@ from kuma.wiki.signals import render_done, restore_done -@mock.patch('kuma.api.signal_handlers.publish') +@mock.patch("kuma.api.signal_handlers.publish") def test_restore_signal(publish_mock, root_doc): """The document is published on the restore_done signal.""" restore_done.send(sender=Document, instance=root_doc) publish_mock.delay.assert_called_once_with([root_doc.pk]) -@pytest.mark.parametrize('invalidate_cdn_cache', (True, False)) -@mock.patch('kuma.api.signal_handlers.publish') +@pytest.mark.parametrize("invalidate_cdn_cache", (True, False)) +@mock.patch("kuma.api.signal_handlers.publish") def test_render_signal(publish_mock, root_doc, invalidate_cdn_cache): """The document is published on the render_done signal.""" - render_done.send(sender=Document, instance=root_doc, - invalidate_cdn_cache=invalidate_cdn_cache) + render_done.send( + sender=Document, instance=root_doc, invalidate_cdn_cache=invalidate_cdn_cache + ) publish_mock.delay.assert_called_once_with( - [root_doc.pk], invalidate_cdn_cache=invalidate_cdn_cache) + [root_doc.pk], invalidate_cdn_cache=invalidate_cdn_cache + ) -@pytest.mark.parametrize('case', ('normal', 'redirect')) -@mock.patch('kuma.api.signal_handlers.unpublish') +@pytest.mark.parametrize("case", ("normal", "redirect")) +@mock.patch("kuma.api.signal_handlers.unpublish") def test_post_delete_signal(unpublish_mock, root_doc, redirect_doc, case): """The document is unpublished after it is deleted.""" - doc = root_doc if case == 'normal' else redirect_doc + doc = root_doc if case == "normal" else redirect_doc doc.delete() unpublish_mock.delay.assert_called_once_with([(doc.locale, doc.slug)]) diff --git a/kuma/api/tests/test_tasks.py b/kuma/api/tests/test_tasks.py index 2dc53419db7..4c5facaad28 100644 --- a/kuma/api/tests/test_tasks.py +++ b/kuma/api/tests/test_tasks.py @@ -1,13 +1,10 @@ - - import json from unittest import mock import pytest from kuma.api.tasks import publish, request_cdn_cache_invalidation, unpublish -from kuma.api.v1.views import ( - document_api_data, get_content_based_redirect, get_s3_key) +from kuma.api.v1.views import document_api_data, get_content_based_redirect, get_s3_key from kuma.wiki.templatetags.jinja_helpers import absolutify @@ -20,40 +17,32 @@ def mocked_get_cloudfront_client(): Avoid that thread-safety problem entirely by mocking the whole function. """ - with mock.patch('kuma.api.tasks.get_cloudfront_client') as mocked: + with mock.patch("kuma.api.tasks.get_cloudfront_client") as mocked: yield mocked def get_mocked_s3_bucket(): - def get_s3_response(**kwargs): - deleted = kwargs['Delete']['Objects'] + deleted = kwargs["Delete"]["Objects"] if len(deleted) > 1: if len(deleted) == 2: # Let's make them all errors. errors = [deleted[0].copy(), deleted[1].copy()] for error in errors: - error.update(Code='InternalError', Message='Some error') + error.update(Code="InternalError", Message="Some error") # S3 excludes the "Deleted" key from its response # if there are none. - return { - 'Errors': errors - } + return {"Errors": errors} # Otherwise, let's make the first one an error. error = deleted[0].copy() - error.update(Code='InternalError', Message='Some error') - return { - 'Deleted': deleted[1:], - 'Errors': [error] - } + error.update(Code="InternalError", Message="Some error") + return {"Deleted": deleted[1:], "Errors": [error]} # S3 excludes the "Errors" key from its response if there are none. - return { - 'Deleted': deleted - } + return {"Deleted": deleted} s3_object_mock = mock.Mock() s3_object_mock.__repr__ = mock.Mock( - side_effect=['S3 Object #1', 'S3 Object #2', 'S3 Object #3'] + side_effect=["S3 Object #1", "S3 Object #2", "S3 Object #3"] ) s3_bucket_mock = mock.Mock() s3_bucket_mock.put_object = mock.Mock(return_value=s3_object_mock) @@ -67,34 +56,34 @@ def test_publish_no_s3_bucket_configured(root_doc): doc_pks = [root_doc.pk] publish(doc_pks, log=log_mock) log_mock.info.assert_called_once_with( - 'Skipping publish of {!r}: no S3 bucket configured'.format(doc_pks)) + "Skipping publish of {!r}: no S3 bucket configured".format(doc_pks) + ) -@pytest.mark.parametrize('invalidate_cdn_cache', (True, False)) -@mock.patch('kuma.api.tasks.get_s3_bucket') +@pytest.mark.parametrize("invalidate_cdn_cache", (True, False)) +@mock.patch("kuma.api.tasks.get_s3_bucket") def test_publish_standard(get_s3_bucket_mock, root_doc, invalidate_cdn_cache): """Test the publish task for a standard (non-redirect) document.""" log_mock = mock.Mock() get_s3_bucket_mock.return_value = s3_bucket_mock = get_mocked_s3_bucket() publish.get_logger = mock.Mock(return_value=log_mock) - with mock.patch('kuma.api.tasks.request_cdn_cache_invalidation') as mocked: + with mock.patch("kuma.api.tasks.request_cdn_cache_invalidation") as mocked: publish([root_doc.pk], invalidate_cdn_cache=invalidate_cdn_cache) if invalidate_cdn_cache: - mocked.delay.assert_called_once_with( - [(root_doc.locale, root_doc.slug)]) + mocked.delay.assert_called_once_with([(root_doc.locale, root_doc.slug)]) else: mocked.delay.assert_not_called() s3_bucket_mock.put_object.assert_called_once_with( - ACL='public-read', + ACL="public-read", Key=get_s3_key(root_doc), Body=json.dumps(document_api_data(root_doc)), - ContentType='application/json', - ContentLanguage=root_doc.locale + ContentType="application/json", + ContentLanguage=root_doc.locale, ) - log_mock.info.assert_called_once_with('Published S3 Object #1') + log_mock.info.assert_called_once_with("Published S3 Object #1") -@mock.patch('kuma.api.tasks.get_s3_bucket') +@mock.patch("kuma.api.tasks.get_s3_bucket") def test_publish_redirect(get_s3_bucket_mock, root_doc, redirect_doc): """ Test the publish task for a document that redirects to another document @@ -105,18 +94,23 @@ def test_publish_redirect(get_s3_bucket_mock, root_doc, redirect_doc): publish([redirect_doc.pk], log=log_mock) s3_bucket_mock.put_object.assert_called_once_with( - ACL='public-read', + ACL="public-read", Key=get_s3_key(redirect_doc), WebsiteRedirectLocation=get_s3_key( - root_doc, prefix_with_forward_slash=True, suffix_file_extension=False), - ContentType='application/json', + root_doc, prefix_with_forward_slash=True, suffix_file_extension=False + ), + ContentType="application/json", ContentLanguage=redirect_doc.locale, - Body=json.dumps(document_api_data(None, redirect_url=get_content_based_redirect(redirect_doc)[0])) + Body=json.dumps( + document_api_data( + None, redirect_url=get_content_based_redirect(redirect_doc)[0] + ) + ), ) - log_mock.info.assert_called_once_with('Published S3 Object #1') + log_mock.info.assert_called_once_with("Published S3 Object #1") -@mock.patch('kuma.api.tasks.get_s3_bucket') +@mock.patch("kuma.api.tasks.get_s3_bucket") def test_publish_redirect_to_home(get_s3_bucket_mock, redirect_to_home): """ Test the publish task for a document that redirects to a URL outside the @@ -126,16 +120,16 @@ def test_publish_redirect_to_home(get_s3_bucket_mock, redirect_to_home): get_s3_bucket_mock.return_value = s3_bucket_mock = get_mocked_s3_bucket() publish([redirect_to_home.pk], log=log_mock) s3_bucket_mock.put_object.assert_called_once_with( - ACL='public-read', + ACL="public-read", Key=get_s3_key(redirect_to_home), - Body=json.dumps(document_api_data(redirect_url='/en-US/')), - ContentType='application/json', - ContentLanguage=redirect_to_home.locale + Body=json.dumps(document_api_data(redirect_url="/en-US/")), + ContentType="application/json", + ContentLanguage=redirect_to_home.locale, ) - log_mock.info.assert_called_once_with('Published S3 Object #1') + log_mock.info.assert_called_once_with("Published S3 Object #1") -@mock.patch('kuma.api.tasks.get_s3_bucket') +@mock.patch("kuma.api.tasks.get_s3_bucket") def test_publish_redirect_to_other(get_s3_bucket_mock, redirect_to_macros): """ Test the publish task for a document that redirects to a URL outside the @@ -145,20 +139,23 @@ def test_publish_redirect_to_other(get_s3_bucket_mock, redirect_to_macros): get_s3_bucket_mock.return_value = s3_bucket_mock = get_mocked_s3_bucket() publish([redirect_to_macros.pk], log=log_mock) s3_bucket_mock.put_object.assert_called_once_with( - ACL='public-read', + ACL="public-read", Key=get_s3_key(redirect_to_macros), - Body=json.dumps(document_api_data( - redirect_url=absolutify('/en-US/dashboards/macros', - for_wiki_site=True))), - ContentType='application/json', - ContentLanguage=redirect_to_macros.locale + Body=json.dumps( + document_api_data( + redirect_url=absolutify("/en-US/dashboards/macros", for_wiki_site=True) + ) + ), + ContentType="application/json", + ContentLanguage=redirect_to_macros.locale, ) - log_mock.info.assert_called_once_with('Published S3 Object #1') + log_mock.info.assert_called_once_with("Published S3 Object #1") -@mock.patch('kuma.api.tasks.get_s3_bucket') -def test_publish_multiple(get_s3_bucket_mock, root_doc, redirect_doc, - redirect_to_home, trans_doc): +@mock.patch("kuma.api.tasks.get_s3_bucket") +def test_publish_multiple( + get_s3_bucket_mock, root_doc, redirect_doc, redirect_to_home, trans_doc +): """ Test the publish task for multiple documents of various kinds, including standard documents and redirects. @@ -166,42 +163,56 @@ def test_publish_multiple(get_s3_bucket_mock, root_doc, redirect_doc, trans_doc.delete() log_mock = mock.Mock() get_s3_bucket_mock.return_value = s3_bucket_mock = get_mocked_s3_bucket() - publish([trans_doc.pk, root_doc.pk, redirect_doc.pk, redirect_to_home.pk], - log=log_mock, completion_message='Done!') - s3_bucket_mock.put_object.assert_has_calls([ - mock.call( - ACL='public-read', - Key=get_s3_key(root_doc), - Body=json.dumps( - document_api_data(root_doc)), - ContentType='application/json', - ContentLanguage=root_doc.locale - ), - mock.call( - ACL='public-read', - Key=get_s3_key(redirect_doc), - WebsiteRedirectLocation=get_s3_key( - root_doc, prefix_with_forward_slash=True, suffix_file_extension=False), - ContentType='application/json', - ContentLanguage=redirect_doc.locale, - Body=json.dumps(document_api_data(redirect_url=get_content_based_redirect(redirect_doc)[0])), - ), - mock.call( - ACL='public-read', - Key=get_s3_key(redirect_to_home), - Body=json.dumps(document_api_data(redirect_url='/en-US/')), - ContentType='application/json', - ContentLanguage=redirect_to_home.locale - ), - ]) + publish( + [trans_doc.pk, root_doc.pk, redirect_doc.pk, redirect_to_home.pk], + log=log_mock, + completion_message="Done!", + ) + s3_bucket_mock.put_object.assert_has_calls( + [ + mock.call( + ACL="public-read", + Key=get_s3_key(root_doc), + Body=json.dumps(document_api_data(root_doc)), + ContentType="application/json", + ContentLanguage=root_doc.locale, + ), + mock.call( + ACL="public-read", + Key=get_s3_key(redirect_doc), + WebsiteRedirectLocation=get_s3_key( + root_doc, + prefix_with_forward_slash=True, + suffix_file_extension=False, + ), + ContentType="application/json", + ContentLanguage=redirect_doc.locale, + Body=json.dumps( + document_api_data( + redirect_url=get_content_based_redirect(redirect_doc)[0] + ) + ), + ), + mock.call( + ACL="public-read", + Key=get_s3_key(redirect_to_home), + Body=json.dumps(document_api_data(redirect_url="/en-US/")), + ContentType="application/json", + ContentLanguage=redirect_to_home.locale, + ), + ] + ) log_mock.error.assert_called_once_with( - 'Document with pk={} does not exist'.format(trans_doc.pk)) - log_mock.info.assert_has_calls([ - mock.call('Published S3 Object #1'), - mock.call('Published S3 Object #2'), - mock.call('Published S3 Object #3'), - mock.call('Done!'), - ]) + "Document with pk={} does not exist".format(trans_doc.pk) + ) + log_mock.info.assert_has_calls( + [ + mock.call("Published S3 Object #1"), + mock.call("Published S3 Object #2"), + mock.call("Published S3 Object #3"), + mock.call("Done!"), + ] + ) def test_unpublish_no_s3_bucket_configured(root_doc): @@ -210,48 +221,46 @@ def test_unpublish_no_s3_bucket_configured(root_doc): doc_locale_slug_pairs = [(root_doc.locale, root_doc.slug)] unpublish(doc_locale_slug_pairs, log=log_mock) log_mock.info.assert_called_once_with( - 'Skipping unpublish of {!r}: no S3 bucket configured'.format( - doc_locale_slug_pairs)) + "Skipping unpublish of {!r}: no S3 bucket configured".format( + doc_locale_slug_pairs + ) + ) -@pytest.mark.parametrize('case', ('un-deleted', 'deleted', 'purged')) -@pytest.mark.parametrize('invalidate_cdn_cache', (True, False)) -@mock.patch('kuma.api.tasks.get_s3_bucket') +@pytest.mark.parametrize("case", ("un-deleted", "deleted", "purged")) +@pytest.mark.parametrize("invalidate_cdn_cache", (True, False)) +@mock.patch("kuma.api.tasks.get_s3_bucket") def test_unpublish(get_s3_bucket_mock, root_doc, invalidate_cdn_cache, case): """Test the unpublish task for a single document.""" - if case in ('deleted', 'purged'): + if case in ("deleted", "purged"): root_doc.deleted = True root_doc.save() - if case == 'purged': + if case == "purged": root_doc.purge() log_mock = mock.Mock() s3_bucket_mock = get_mocked_s3_bucket() get_s3_bucket_mock.return_value = s3_bucket_mock unpublish.get_logger = mock.Mock(return_value=log_mock) - with mock.patch('kuma.api.tasks.request_cdn_cache_invalidation') as mocked: - unpublish([(root_doc.locale, root_doc.slug)], - invalidate_cdn_cache=invalidate_cdn_cache) + with mock.patch("kuma.api.tasks.request_cdn_cache_invalidation") as mocked: + unpublish( + [(root_doc.locale, root_doc.slug)], + invalidate_cdn_cache=invalidate_cdn_cache, + ) if invalidate_cdn_cache: - mocked.delay.assert_called_once_with( - [(root_doc.locale, root_doc.slug)]) + mocked.delay.assert_called_once_with([(root_doc.locale, root_doc.slug)]) else: mocked.delay.assert_not_called() s3_key = get_s3_key(root_doc) s3_bucket_mock.delete_objects.assert_called_once_with( - Delete={ - 'Objects': [ - { - 'Key': s3_key - } - ] - } + Delete={"Objects": [{"Key": s3_key}]} ) - log_mock.info.assert_called_once_with('Unpublished {}'.format(s3_key)) + log_mock.info.assert_called_once_with("Unpublished {}".format(s3_key)) -@mock.patch('kuma.api.tasks.get_s3_bucket') -def test_unpublish_multiple(get_s3_bucket_mock, root_doc, redirect_doc, - redirect_to_home): +@mock.patch("kuma.api.tasks.get_s3_bucket") +def test_unpublish_multiple( + get_s3_bucket_mock, root_doc, redirect_doc, redirect_to_home +): """ Test the unpublish task for multiple documents of various kinds, including standard documents and redirects. @@ -260,31 +269,25 @@ def test_unpublish_multiple(get_s3_bucket_mock, root_doc, redirect_doc, docs = (root_doc, redirect_doc, redirect_to_home) doc_locale_slug_pairs = [(doc.locale, doc.slug) for doc in docs] get_s3_bucket_mock.return_value = s3_bucket_mock = get_mocked_s3_bucket() - unpublish(doc_locale_slug_pairs, log=log_mock, completion_message='Done!') + unpublish(doc_locale_slug_pairs, log=log_mock, completion_message="Done!") s3_keys = tuple(get_s3_key(doc) for doc in docs) s3_bucket_mock.delete_objects.assert_called_once_with( - Delete={ - 'Objects': [ - { - 'Key': key - } - for key in s3_keys - ] - } + Delete={"Objects": [{"Key": key} for key in s3_keys]} ) log_mock.error.assert_called_once_with( - 'Unable to unpublish {}: (InternalError) Some error'.format(s3_keys[0]) + "Unable to unpublish {}: (InternalError) Some error".format(s3_keys[0]) ) log_mock.info.assert_has_calls( - [mock.call('Unpublished {}'.format(key)) for key in s3_keys[1:]] + - [mock.call('Done!')] + [mock.call("Unpublished {}".format(key)) for key in s3_keys[1:]] + + [mock.call("Done!")] ) -@mock.patch('kuma.api.tasks.get_s3_bucket') -@mock.patch('kuma.api.tasks.S3_MAX_KEYS_PER_DELETE', 2) -def test_unpublish_multiple_chunked(get_s3_bucket_mock, root_doc, redirect_doc, - redirect_to_home): +@mock.patch("kuma.api.tasks.get_s3_bucket") +@mock.patch("kuma.api.tasks.S3_MAX_KEYS_PER_DELETE", 2) +def test_unpublish_multiple_chunked( + get_s3_bucket_mock, root_doc, redirect_doc, redirect_to_home +): """ Test the unpublish task for multiple documents where the deletes are broken-up into chunks. @@ -293,44 +296,27 @@ def test_unpublish_multiple_chunked(get_s3_bucket_mock, root_doc, redirect_doc, docs = (root_doc, redirect_doc, redirect_to_home) doc_locale_slug_pairs = [(doc.locale, doc.slug) for doc in docs] get_s3_bucket_mock.return_value = s3_bucket_mock = get_mocked_s3_bucket() - unpublish(doc_locale_slug_pairs, log=log_mock, completion_message='Done!') + unpublish(doc_locale_slug_pairs, log=log_mock, completion_message="Done!") s3_keys = tuple(get_s3_key(doc) for doc in docs) - s3_bucket_mock.delete_objects.assert_has_calls([ - mock.call( - Delete={ - 'Objects': [ - { - 'Key': key - } - for key in s3_keys[:2] - ] - } - ), - mock.call( - Delete={ - 'Objects': [ - { - 'Key': key - } - for key in s3_keys[2:] - ] - } - ) - ]) - log_mock.error.assert_has_calls([ - mock.call( - 'Unable to unpublish {}: (InternalError) Some error'.format(key)) - for key in s3_keys[:2] - ]) - log_mock.info.assert_has_calls([ - mock.call('Unpublished {}'.format(s3_keys[-1])), - mock.call('Done!') - ]) + s3_bucket_mock.delete_objects.assert_has_calls( + [ + mock.call(Delete={"Objects": [{"Key": key} for key in s3_keys[:2]]}), + mock.call(Delete={"Objects": [{"Key": key} for key in s3_keys[2:]]}), + ] + ) + log_mock.error.assert_has_calls( + [ + mock.call("Unable to unpublish {}: (InternalError) Some error".format(key)) + for key in s3_keys[:2] + ] + ) + log_mock.info.assert_has_calls( + [mock.call("Unpublished {}".format(s3_keys[-1])), mock.call("Done!")] + ) def test_request_cdn_cache_invalidation_not_configured( - settings, - mocked_get_cloudfront_client + settings, mocked_get_cloudfront_client ): """When the settings.MDN_CLOUDFRONT_DISTRIBUTIONS isn't set, no calls should be make to the boto3 CloudFront client. @@ -339,7 +325,7 @@ def test_request_cdn_cache_invalidation_not_configured( # should be set to an empty dict. Just sanity-check that. assert not settings.MDN_CLOUDFRONT_DISTRIBUTIONS - pairs = [('sv-SE', 'Learn/stuff')] + pairs = [("sv-SE", "Learn/stuff")] request_cdn_cache_invalidation(pairs) mocked_get_cloudfront_client().assert_not_called() @@ -353,45 +339,36 @@ def test_request_cdn_cache_invalidation_not_configured( def transformer(locale, slug): transform_calls_made.append([locale, slug]) - return '/' + locale + '/' + slug + '/' + return "/" + locale + "/" + slug + "/" def test_request_cdn_cache_invalidation_configured( - settings, - mocked_get_cloudfront_client + settings, mocked_get_cloudfront_client ): """When explicitly enabling a MDN_CLOUDFRONT_DISTRIBUTIONS we should expect its 'transform' function to be called. """ settings.MDN_CLOUDFRONT_DISTRIBUTIONS = { - 'mything': { - 'id': 'XYZABC123', - 'transform_function': ( - 'kuma.api.tests.test_tasks.transformer' - ) + "mything": { + "id": "XYZABC123", + "transform_function": ("kuma.api.tests.test_tasks.transformer"), }, - 'unconfigured': { - 'id': None, - 'transform_function': 'wont.be.used' - } + "unconfigured": {"id": None, "transform_function": "wont.be.used"}, } - pairs = [('sv-SE', 'Learn/stuff')] + pairs = [("sv-SE", "Learn/stuff")] request_cdn_cache_invalidation(pairs) - assert transform_calls_made == [['sv-SE', 'Learn/stuff']] + assert transform_calls_made == [["sv-SE", "Learn/stuff"]] # When used, we need to reset it because it's a module global mutable # specific to this test module. del transform_calls_made[:] mocked_get_cloudfront_client().create_invalidation.assert_called_with( - DistributionId='XYZABC123', + DistributionId="XYZABC123", InvalidationBatch={ - 'Paths': { - 'Items': ['/sv-SE/Learn/stuff/'], - 'Quantity': 1 - }, - 'CallerReference': mock.ANY - } + "Paths": {"Items": ["/sv-SE/Learn/stuff/"], "Quantity": 1}, + "CallerReference": mock.ANY, + }, ) diff --git a/kuma/api/urls.py b/kuma/api/urls.py index be576dbfee2..a190298b040 100644 --- a/kuma/api/urls.py +++ b/kuma/api/urls.py @@ -2,5 +2,5 @@ urlpatterns = [ - url('^v1/', include('kuma.api.v1.urls')), + url("^v1/", include("kuma.api.v1.urls")), ] diff --git a/kuma/api/v1/serializers.py b/kuma/api/v1/serializers.py index c49a18f1b8d..06aac7e496f 100644 --- a/kuma/api/v1/serializers.py +++ b/kuma/api/v1/serializers.py @@ -9,21 +9,16 @@ class BCSignalSerializer(serializers.Serializer): browsers = serializers.CharField(max_length=255) slug = serializers.CharField(max_length=255) locale = serializers.CharField(max_length=7) - explanation = serializers.CharField( - allow_blank=True, - max_length=1000 - ) + explanation = serializers.CharField(allow_blank=True, max_length=1000) supporting_material = serializers.CharField( - allow_blank=True, - required=False, - max_length=1000 + allow_blank=True, required=False, max_length=1000 ) def create(self, validated_data): - slug = validated_data.pop('slug') - locale = validated_data.pop('locale') + slug = validated_data.pop("slug") + locale = validated_data.pop("locale") document = Document.objects.filter(slug=slug, locale=locale).first() if document: return BCSignal.objects.create(document=document, **validated_data) - raise exceptions.ValidationError('Document not found') + raise exceptions.ValidationError("Document not found") diff --git a/kuma/api/v1/tests/test_views.py b/kuma/api/v1/tests/test_views.py index 1ab90ff6903..98c3fd7df41 100644 --- a/kuma/api/v1/tests/test_views.py +++ b/kuma/api/v1/tests/test_views.py @@ -1,11 +1,8 @@ - - import pytest from django.conf import settings from waffle.models import Flag, Sample, Switch -from kuma.api.v1.views import (document_api_data, get_content_based_redirect, - get_s3_key) +from kuma.api.v1.views import document_api_data, get_content_based_redirect, get_s3_key from kuma.core.tests import assert_no_cache_header from kuma.core.urlresolvers import reverse from kuma.search.tests import ElasticTestCase @@ -14,54 +11,49 @@ def test_get_s3_key(root_doc): locale, slug = root_doc.locale, root_doc.slug - expected_key = 'api/v1/doc/{}/{}.json'.format(locale, slug) + expected_key = "api/v1/doc/{}/{}.json".format(locale, slug) + assert get_s3_key(root_doc) == get_s3_key(locale=locale, slug=slug) == expected_key assert ( - get_s3_key(root_doc) == get_s3_key(locale=locale, slug=slug) == - expected_key - ) - assert ( - get_s3_key(root_doc, prefix_with_forward_slash=True) == - get_s3_key(locale=locale, slug=slug, prefix_with_forward_slash=True) == - '/' + expected_key + get_s3_key(root_doc, prefix_with_forward_slash=True) + == get_s3_key(locale=locale, slug=slug, prefix_with_forward_slash=True) + == "/" + expected_key ) -@pytest.mark.parametrize('case', ('normal', - 'redirect', - 'redirect-to-self', - 'redirect-to-home', - 'redirect-to-wiki')) -def test_get_content_based_redirect(root_doc, redirect_doc, redirect_to_self, - redirect_to_home, redirect_to_macros, case): - if case == 'normal': +@pytest.mark.parametrize( + "case", + ("normal", "redirect", "redirect-to-self", "redirect-to-home", "redirect-to-wiki"), +) +def test_get_content_based_redirect( + root_doc, redirect_doc, redirect_to_self, redirect_to_home, redirect_to_macros, case +): + if case == "normal": doc = root_doc expected = None - elif case == 'redirect': + elif case == "redirect": doc = redirect_doc expected = ( get_s3_key( - root_doc, - prefix_with_forward_slash=True, - suffix_file_extension=False), - True) - elif case == 'redirect-to-self': + root_doc, prefix_with_forward_slash=True, suffix_file_extension=False + ), + True, + ) + elif case == "redirect-to-self": doc = redirect_to_self expected = None - elif case == 'redirect-to-home': + elif case == "redirect-to-home": doc = redirect_to_home - expected = ('/en-US/', False) + expected = ("/en-US/", False) else: doc = redirect_to_macros - expected = ( - absolutify('/en-US/dashboards/macros', for_wiki_site=True), False) + expected = (absolutify("/en-US/dashboards/macros", for_wiki_site=True), False) assert get_content_based_redirect(doc) == expected -@pytest.mark.parametrize( - 'http_method', ['put', 'post', 'delete', 'options', 'head']) +@pytest.mark.parametrize("http_method", ["put", "post", "delete", "options", "head"]) def test_doc_api_disallowed_methods(client, http_method): """HTTP methods other than GET are not allowed.""" - url = reverse('api.v1.doc', args=['en-US', 'Web/CSS']) + url = reverse("api.v1.doc", args=["en-US", "Web/CSS"]) response = getattr(client, http_method)(url) assert response.status_code == 405 assert_no_cache_header(response) @@ -69,7 +61,7 @@ def test_doc_api_disallowed_methods(client, http_method): def test_doc_api_404(client, root_doc): """We get a 404 if we ask for a document that does not exist.""" - url = reverse('api.v1.doc', args=['en-US', 'NoSuchPage']) + url = reverse("api.v1.doc", args=["en-US", "NoSuchPage"]) response = client.get(url) assert response.status_code == 404 assert_no_cache_header(response) @@ -77,37 +69,40 @@ def test_doc_api_404(client, root_doc): def test_doc_api(client, trans_doc): """On success we get document details in a JSON response.""" - url = reverse('api.v1.doc', args=[trans_doc.locale, trans_doc.slug]) + url = reverse("api.v1.doc", args=[trans_doc.locale, trans_doc.slug]) response = client.get(url) assert response.status_code == 200 assert_no_cache_header(response) data = response.json() - assert data['documentData'] - assert data['redirectURL'] is None - doc_data = data['documentData'] - assert doc_data['locale'] == trans_doc.locale - assert doc_data['slug'] == trans_doc.slug - assert doc_data['id'] == trans_doc.id - assert doc_data['title'] == trans_doc.title - assert doc_data['language'] == trans_doc.language - assert doc_data['hrefLang'] == 'fr' - assert doc_data['absoluteURL'] == trans_doc.get_absolute_url() - assert doc_data['wikiURL'] == absolutify(trans_doc.get_absolute_url(), - for_wiki_site=True) - assert doc_data['translateURL'] is None - assert doc_data['bodyHTML'] == trans_doc.get_body_html() - assert doc_data['quickLinksHTML'] == trans_doc.get_quick_links_html() - assert doc_data['tocHTML'] == trans_doc.get_toc_html() - assert doc_data['translations'] == [{ - 'locale': 'en-US', - 'language': 'English (US)', - 'hrefLang': 'en', - 'localizedLanguage': 'Anglais am\u00e9ricain', - 'title': 'Root Document', - 'url': '/en-US/docs/Root' - }] - assert doc_data['lastModified'] == '2017-04-14T12:20:00' + assert data["documentData"] + assert data["redirectURL"] is None + doc_data = data["documentData"] + assert doc_data["locale"] == trans_doc.locale + assert doc_data["slug"] == trans_doc.slug + assert doc_data["id"] == trans_doc.id + assert doc_data["title"] == trans_doc.title + assert doc_data["language"] == trans_doc.language + assert doc_data["hrefLang"] == "fr" + assert doc_data["absoluteURL"] == trans_doc.get_absolute_url() + assert doc_data["wikiURL"] == absolutify( + trans_doc.get_absolute_url(), for_wiki_site=True + ) + assert doc_data["translateURL"] is None + assert doc_data["bodyHTML"] == trans_doc.get_body_html() + assert doc_data["quickLinksHTML"] == trans_doc.get_quick_links_html() + assert doc_data["tocHTML"] == trans_doc.get_toc_html() + assert doc_data["translations"] == [ + { + "locale": "en-US", + "language": "English (US)", + "hrefLang": "en", + "localizedLanguage": "Anglais am\u00e9ricain", + "title": "Root Document", + "url": "/en-US/docs/Root", + } + ] + assert doc_data["lastModified"] == "2017-04-14T12:20:00" def test_doc_api_for_redirect_to_doc(client, root_doc, redirect_doc): @@ -115,137 +110,136 @@ def test_doc_api_for_redirect_to_doc(client, root_doc, redirect_doc): Test the document API when we're requesting data for a document that redirects to another document. """ - url = reverse('api.v1.doc', args=[redirect_doc.locale, redirect_doc.slug]) + url = reverse("api.v1.doc", args=[redirect_doc.locale, redirect_doc.slug]) response = client.get(url, follow=True) assert response.status_code == 200 assert_no_cache_header(response) data = response.json() - assert data['documentData'] - assert data['redirectURL'] is None - doc_data = data['documentData'] - assert doc_data['locale'] == root_doc.locale - assert doc_data['slug'] == root_doc.slug - assert doc_data['id'] == root_doc.id - assert doc_data['title'] == root_doc.title - assert doc_data['language'] == root_doc.language - assert doc_data['hrefLang'] == 'en' - assert doc_data['absoluteURL'] == root_doc.get_absolute_url() - assert doc_data['wikiURL'] == absolutify(root_doc.get_absolute_url(), - for_wiki_site=True) - assert doc_data['translateURL'] == absolutify( - reverse( - 'wiki.select_locale', - args=(root_doc.slug,), - locale=root_doc.locale, - ), - for_wiki_site=True + assert data["documentData"] + assert data["redirectURL"] is None + doc_data = data["documentData"] + assert doc_data["locale"] == root_doc.locale + assert doc_data["slug"] == root_doc.slug + assert doc_data["id"] == root_doc.id + assert doc_data["title"] == root_doc.title + assert doc_data["language"] == root_doc.language + assert doc_data["hrefLang"] == "en" + assert doc_data["absoluteURL"] == root_doc.get_absolute_url() + assert doc_data["wikiURL"] == absolutify( + root_doc.get_absolute_url(), for_wiki_site=True ) - assert doc_data['bodyHTML'] == root_doc.get_body_html() - assert doc_data['quickLinksHTML'] == root_doc.get_quick_links_html() - assert doc_data['tocHTML'] == root_doc.get_toc_html() - assert doc_data['translations'] == [] - assert doc_data['lastModified'] == '2017-04-14T12:15:00' + assert doc_data["translateURL"] == absolutify( + reverse("wiki.select_locale", args=(root_doc.slug,), locale=root_doc.locale,), + for_wiki_site=True, + ) + assert doc_data["bodyHTML"] == root_doc.get_body_html() + assert doc_data["quickLinksHTML"] == root_doc.get_quick_links_html() + assert doc_data["tocHTML"] == root_doc.get_toc_html() + assert doc_data["translations"] == [] + assert doc_data["lastModified"] == "2017-04-14T12:15:00" -@pytest.mark.parametrize('case', ('redirect-to-home', 'redirect-to-other')) -def test_doc_api_for_redirect_to_non_doc(client, redirect_to_home, - redirect_to_macros, case): +@pytest.mark.parametrize("case", ("redirect-to-home", "redirect-to-other")) +def test_doc_api_for_redirect_to_non_doc( + client, redirect_to_home, redirect_to_macros, case +): """ Test the document API when we're requesting data for a document that redirects to a non-document page (either the home page or another). """ - if case == 'redirect-to-home': + if case == "redirect-to-home": doc = redirect_to_home - expected_redirect_url = '/en-US/' + expected_redirect_url = "/en-US/" else: doc = redirect_to_macros - expected_redirect_url = absolutify('/en-US/dashboards/macros', - for_wiki_site=True) - url = reverse('api.v1.doc', args=[doc.locale, doc.slug]) + expected_redirect_url = absolutify( + "/en-US/dashboards/macros", for_wiki_site=True + ) + url = reverse("api.v1.doc", args=[doc.locale, doc.slug]) response = client.get(url) assert response.status_code == 200 assert_no_cache_header(response) data = response.json() - assert data['documentData'] is None - assert data['redirectURL'] == expected_redirect_url + assert data["documentData"] is None + assert data["redirectURL"] == expected_redirect_url # Also ensure that we get exactly the same data by calling # the document_api_data() function directly assert data == document_api_data(redirect_url=expected_redirect_url) -@pytest.mark.parametrize( - 'http_method', ['put', 'post', 'delete', 'options', 'head']) +@pytest.mark.parametrize("http_method", ["put", "post", "delete", "options", "head"]) def test_whoami_disallowed_methods(client, http_method): """HTTP methods other than GET are not allowed.""" - url = reverse('api.v1.whoami') + url = reverse("api.v1.whoami") response = getattr(client, http_method)(url) assert response.status_code == 405 assert_no_cache_header(response) @pytest.mark.django_db -@pytest.mark.parametrize('timezone', ('US/Eastern', 'US/Pacific')) +@pytest.mark.parametrize("timezone", ("US/Eastern", "US/Pacific")) def test_whoami_anonymous(client, settings, timezone): """Test response for anonymous users.""" # Create some fake waffle objects - Flag.objects.create(name='section_edit', authenticated=True) - Flag.objects.create(name='flag_all', everyone=True) - Flag.objects.create(name='flag_none', percent=0) + Flag.objects.create(name="section_edit", authenticated=True) + Flag.objects.create(name="flag_all", everyone=True) + Flag.objects.create(name="flag_none", percent=0) Switch.objects.create(name="switch_on", active=True) Switch.objects.create(name="switch_off", active=False) Sample.objects.create(name="sample_never", percent=0) Sample.objects.create(name="sample_always", percent=100) settings.TIME_ZONE = timezone - url = reverse('api.v1.whoami') + url = reverse("api.v1.whoami") response = client.get(url) assert response.status_code == 200 - assert response['content-type'] == 'application/json' + assert response["content-type"] == "application/json" assert response.json() == { - 'username': None, - 'timezone': timezone, - 'is_authenticated': False, - 'is_staff': False, - 'is_superuser': False, - 'is_beta_tester': False, - 'avatar_url': None, - 'waffle': { - 'flags': { - 'section_edit': False, - 'flag_all': True, - 'flag_none': False, - 'subscription': False + "username": None, + "timezone": timezone, + "is_authenticated": False, + "is_staff": False, + "is_superuser": False, + "is_beta_tester": False, + "avatar_url": None, + "waffle": { + "flags": { + "section_edit": False, + "flag_all": True, + "flag_none": False, + "subscription": False, }, - 'switches': { - 'switch_on': True, - 'switch_off': False - }, - 'samples': { - 'sample_always': True, - 'sample_never': False - } - } + "switches": {"switch_on": True, "switch_off": False}, + "samples": {"sample_always": True, "sample_never": False}, + }, } assert_no_cache_header(response) @pytest.mark.django_db @pytest.mark.parametrize( - 'timezone,is_staff,is_superuser,is_beta_tester', - [('US/Eastern', False, False, False), - ('US/Pacific', True, True, True)], - ids=('muggle', 'wizard')) -def test_whoami(user_client, wiki_user, wiki_user_github_account, - beta_testers_group, timezone, is_staff, is_superuser, - is_beta_tester): + "timezone,is_staff,is_superuser,is_beta_tester", + [("US/Eastern", False, False, False), ("US/Pacific", True, True, True)], + ids=("muggle", "wizard"), +) +def test_whoami( + user_client, + wiki_user, + wiki_user_github_account, + beta_testers_group, + timezone, + is_staff, + is_superuser, + is_beta_tester, +): """Test responses for logged-in users.""" # Create some fake waffle objects - Flag.objects.create(name='section_edit', authenticated=True) - Flag.objects.create(name='flag_all', everyone=True) - Flag.objects.create(name='flag_none', percent=0, superusers=False) + Flag.objects.create(name="section_edit", authenticated=True) + Flag.objects.create(name="flag_all", everyone=True) + Flag.objects.create(name="flag_none", percent=0, superusers=False) Switch.objects.create(name="switch_on", active=True) Switch.objects.create(name="switch_off", active=False) Sample.objects.create(name="sample_never", percent=0) @@ -258,136 +252,135 @@ def test_whoami(user_client, wiki_user, wiki_user_github_account, if is_beta_tester: wiki_user.groups.add(beta_testers_group) wiki_user.save() - url = reverse('api.v1.whoami') + url = reverse("api.v1.whoami") response = user_client.get(url) assert response.status_code == 200 - assert response['content-type'] == 'application/json' + assert response["content-type"] == "application/json" assert response.json() == { - 'username': wiki_user.username, - 'timezone': timezone, - 'is_authenticated': True, - 'is_staff': is_staff, - 'is_superuser': is_superuser, - 'is_beta_tester': is_beta_tester, - 'avatar_url': wiki_user_github_account.get_avatar_url(), - 'waffle': { - 'flags': { - 'section_edit': True, - 'flag_all': True, - 'flag_none': False, - 'subscription': is_staff + "username": wiki_user.username, + "timezone": timezone, + "is_authenticated": True, + "is_staff": is_staff, + "is_superuser": is_superuser, + "is_beta_tester": is_beta_tester, + "avatar_url": wiki_user_github_account.get_avatar_url(), + "waffle": { + "flags": { + "section_edit": True, + "flag_all": True, + "flag_none": False, + "subscription": is_staff, }, - 'switches': { - 'switch_on': True, - 'switch_off': False - }, - 'samples': { - 'sample_always': True, - 'sample_never': False - } - } + "switches": {"switch_on": True, "switch_off": False}, + "samples": {"sample_always": True, "sample_never": False}, + }, } assert_no_cache_header(response) @pytest.mark.django_db def test_search_validation_problems(user_client): - url = reverse('api.v1.search', args=['en-US']) + url = reverse("api.v1.search", args=["en-US"]) # 'q' not present response = user_client.get(url) assert response.status_code == 400 - assert response.json()['error'] == "Search term 'q' must be set" + assert response.json()["error"] == "Search term 'q' must be set" # 'q' present but falsy - response = user_client.get(url, {'q': ''}) + response = user_client.get(url, {"q": ""}) assert response.status_code == 400 - assert response.json()['error'] == "Search term 'q' must be set" + assert response.json()["error"] == "Search term 'q' must be set" # 'q' present but locale invalid - response = user_client.get(url, {'q': 'x', 'locale': 'xxx'}) + response = user_client.get(url, {"q": "x", "locale": "xxx"}) assert response.status_code == 400 - assert response.json()['error'] == 'Not a valid locale code' + assert response.json()["error"] == "Not a valid locale code" # 'q' present but contains new line - response = user_client.get(url, {'q': r'test\nsomething'}) + response = user_client.get(url, {"q": r"test\nsomething"}) assert response.status_code == 400 - assert response.json()['q'] == ["Search term must not contain new line"] + assert response.json()["q"] == ["Search term must not contain new line"] # 'q' present but exceeds max allowed characters - response = user_client.get(url, {'q': 'x' * (settings.ES_Q_MAXLENGTH + 1)}) + response = user_client.get(url, {"q": "x" * (settings.ES_Q_MAXLENGTH + 1)}) assert response.status_code == 400 - assert ( - response.json()['q'] == - [f"Ensure this field has no more than {settings.ES_Q_MAXLENGTH} characters."] - ) + assert response.json()["q"] == [ + f"Ensure this field has no more than {settings.ES_Q_MAXLENGTH} characters." + ] class SearchViewTests(ElasticTestCase): - fixtures = ElasticTestCase.fixtures + ['wiki/documents.json', - 'search/filters.json'] + fixtures = ElasticTestCase.fixtures + ["wiki/documents.json", "search/filters.json"] def test_search_basic(self): - url = reverse('api.v1.search', args=['en-US']) - response = self.client.get(url, {'q': 'article'}) + url = reverse("api.v1.search", args=["en-US"]) + response = self.client.get(url, {"q": "article"}) assert response.status_code == 200 - assert response['content-type'] == 'application/json' + assert response["content-type"] == "application/json" data = response.json() - assert data['documents'] - assert data['count'] == 4 - assert data['locale'] == 'en-US' + assert data["documents"] + assert data["count"] == 4 + assert data["locale"] == "en-US" # Now search in a non-en-US locale - response = self.client.get(url, {'q': 'title', 'locale': 'fr'}) + response = self.client.get(url, {"q": "title", "locale": "fr"}) assert response.status_code == 200 - assert response['content-type'] == 'application/json' + assert response["content-type"] == "application/json" data = response.json() - assert data['documents'] - assert data['count'] == 5 - assert data['locale'] == 'fr' + assert data["documents"] + assert data["count"] == 5 + assert data["locale"] == "fr" -@pytest.mark.parametrize('http_method', ('put', 'post', 'delete', 'options')) +@pytest.mark.parametrize("http_method", ("put", "post", "delete", "options")) def test_get_user_disallowed_methods(client, wiki_user, http_method): """ HTTP methods other than GET and HEAD are not allowed on the api.v1.get_user endpoint. """ - url = reverse('api.v1.get_user', args=(wiki_user.username,)) + url = reverse("api.v1.get_user", args=(wiki_user.username,)) response = getattr(client, http_method)(url) assert response.status_code == 405 assert_no_cache_header(response) -@pytest.mark.parametrize('case', ('upper', 'lower')) -@pytest.mark.parametrize('http_method', ('get', 'head')) -def test_get_existing_user(client, wiki_user, wiki_user_github_account, - http_method, case): +@pytest.mark.parametrize("case", ("upper", "lower")) +@pytest.mark.parametrize("http_method", ("get", "head")) +def test_get_existing_user( + client, wiki_user, wiki_user_github_account, http_method, case +): """ Test GET and HEAD on the api.v1.get_user endpoint for an existing user, and also that the username is case insensitive. """ username = getattr(str, case)(wiki_user.username) - url = reverse('api.v1.get_user', args=(username,)) + url = reverse("api.v1.get_user", args=(username,)) response = getattr(client, http_method)(url) assert response.status_code == 200 - assert response['content-type'] == 'application/json' + assert response["content-type"] == "application/json" assert_no_cache_header(response) - if http_method == 'get': + if http_method == "get": data = response.json() - assert data['username'] == wiki_user.username - assert data['avatar_url'] == wiki_user_github_account.get_avatar_url() - for field in ('title', 'fullname', 'organization', 'location', - 'timezone', 'locale'): + assert data["username"] == wiki_user.username + assert data["avatar_url"] == wiki_user_github_account.get_avatar_url() + for field in ( + "title", + "fullname", + "organization", + "location", + "timezone", + "locale", + ): assert data[field] == getattr(wiki_user, field) -@pytest.mark.parametrize('http_method', ('get', 'head')) +@pytest.mark.parametrize("http_method", ("get", "head")) def test_get_nonexisting_user(db, client, http_method): """ Test GET and HEAD on the api.v1.get_user endpoint for a non-existing user. """ - url = reverse('api.v1.get_user', args=('nonexistent',)) + url = reverse("api.v1.get_user", args=("nonexistent",)) response = getattr(client, http_method)(url) assert response.status_code == 404 assert_no_cache_header(response) diff --git a/kuma/api/v1/urls.py b/kuma/api/v1/urls.py index 49f843a20ce..66e58b456e4 100644 --- a/kuma/api/v1/urls.py +++ b/kuma/api/v1/urls.py @@ -4,19 +4,9 @@ urlpatterns = [ - url(r'^doc/(?P[^/]+)/(?P.*)$', - views.doc, - name='api.v1.doc'), - url(r'^whoami/?$', - views.whoami, - name='api.v1.whoami'), - url(r'^search/(?P[^/]+)/?$', - views.search, - name='api.v1.search'), - url(r'^bc-signal/?$', - views.bc_signal, - name='api.v1.bc_signal'), - url(r'^users/(?P[^/]+)/?$', - views.get_user, - name='api.v1.get_user'), + url(r"^doc/(?P[^/]+)/(?P.*)$", views.doc, name="api.v1.doc"), + url(r"^whoami/?$", views.whoami, name="api.v1.whoami"), + url(r"^search/(?P[^/]+)/?$", views.search, name="api.v1.search"), + url(r"^bc-signal/?$", views.bc_signal, name="api.v1.bc_signal"), + url(r"^users/(?P[^/]+)/?$", views.get_user, name="api.v1.get_user"), ] diff --git a/kuma/api/v1/views.py b/kuma/api/v1/views.py index c9d11765b95..12f263eb9b0 100644 --- a/kuma/api/v1/views.py +++ b/kuma/api/v1/views.py @@ -18,7 +18,8 @@ KeywordQueryBackend, LanguageFilterBackend, SearchQueryBackend, - TagGroupFilterBackend) + TagGroupFilterBackend, +) from kuma.search.search import SearchView from kuma.users.models import User from kuma.users.templatetags.jinja_helpers import get_avatar_url @@ -57,18 +58,22 @@ def doc(request, locale, slug): return JsonResponse(document_api_data(document)) -def get_s3_key(doc=None, locale=None, slug=None, - prefix_with_forward_slash=False, - suffix_file_extension=True): +def get_s3_key( + doc=None, + locale=None, + slug=None, + prefix_with_forward_slash=False, + suffix_file_extension=True, +): if doc: locale, slug = doc.locale, doc.slug - key = reverse('api.v1.doc', args=(locale, slug)) + key = reverse("api.v1.doc", args=(locale, slug)) if suffix_file_extension: - key += '.json' + key += ".json" if prefix_with_forward_slash: # Redirects within an S3 bucket must be prefixed with "/". return key - return key.lstrip('/') + return key.lstrip("/") def get_cdn_key(locale, slug): @@ -77,7 +82,8 @@ def get_cdn_key(locale, slug): locale=locale, slug=slug, prefix_with_forward_slash=True, - suffix_file_extension=False) + suffix_file_extension=False, + ) def get_content_based_redirect(document): @@ -98,18 +104,18 @@ def get_content_based_redirect(document): get_s3_key( redirect_document, prefix_with_forward_slash=True, - suffix_file_extension=False), - True + suffix_file_extension=False, + ), + True, ) # This is a redirect to non-document page. For now, if it's the home # page, return a relative path (so we stay on the read-only domain), # otherwise return the full URL for the wiki site. locale = document.locale - is_home_page = (redirect_url in - ('/', '/' + locale, '/{}/'.format(locale))) + is_home_page = redirect_url in ("/", "/" + locale, "/{}/".format(locale)) if is_home_page: # Let's return a relative URL to the home page for this locale. - return ('/{}/'.format(locale), False) + return ("/{}/".format(locale), False) # Otherwise, let's return a full URL to the Wiki site. return (absolutify(redirect_url, for_wiki_site=True), False) return None @@ -121,77 +127,74 @@ def document_api_data(doc=None, redirect_url=None): """ if redirect_url: return { - 'documentData': None, - 'redirectURL': redirect_url, + "documentData": None, + "redirectURL": redirect_url, } # The original english slug for this document, for google analytics - if doc.locale == 'en-US': + if doc.locale == "en-US": en_slug = doc.slug - elif doc.parent_id and doc.parent.locale == 'en-US': + elif doc.parent_id and doc.parent.locale == "en-US": en_slug = doc.parent.slug else: - en_slug = '' + en_slug = "" other_translations = doc.get_other_translations( - fields=('locale', 'slug', 'title', 'parent')) - available_locales = ( - {doc.locale} | set(t.locale for t in other_translations)) + fields=("locale", "slug", "title", "parent") + ) + available_locales = {doc.locale} | set(t.locale for t in other_translations) doc_absolute_url = doc.get_absolute_url() revision = doc.current_or_latest_revision() translation_status = None if doc.parent_id and revision and revision.localization_in_progress: - translation_status = 'outdated' if revision.translation_age >= 10 else 'in-progress' + translation_status = ( + "outdated" if revision.translation_age >= 10 else "in-progress" + ) return { - 'documentData': { - 'locale': doc.locale, - 'slug': doc.slug, - 'enSlug': en_slug, - 'id': doc.id, - 'title': doc.title, - 'summary': doc.get_summary_html(), - 'language': doc.language, - 'hrefLang': doc.get_hreflang(available_locales), - 'absoluteURL': doc_absolute_url, - 'wikiURL': absolutify(doc_absolute_url, for_wiki_site=True), - 'translateURL': ( + "documentData": { + "locale": doc.locale, + "slug": doc.slug, + "enSlug": en_slug, + "id": doc.id, + "title": doc.title, + "summary": doc.get_summary_html(), + "language": doc.language, + "hrefLang": doc.get_hreflang(available_locales), + "absoluteURL": doc_absolute_url, + "wikiURL": absolutify(doc_absolute_url, for_wiki_site=True), + "translateURL": ( absolutify( - reverse( - 'wiki.select_locale', - args=(doc.slug,), - locale=doc.locale, - ), - for_wiki_site=True + reverse("wiki.select_locale", args=(doc.slug,), locale=doc.locale,), + for_wiki_site=True, ) - if doc.is_localizable else - None + if doc.is_localizable + else None ), - 'translationStatus': translation_status, - 'bodyHTML': doc.get_body_html(), - 'quickLinksHTML': doc.get_quick_links_html(), - 'tocHTML': doc.get_toc_html(), - 'raw': doc.html, - 'parents': [ - { - 'url': d.get_absolute_url(), - 'title': d.title - } for d in doc.parents + "translationStatus": translation_status, + "bodyHTML": doc.get_body_html(), + "quickLinksHTML": doc.get_quick_links_html(), + "tocHTML": doc.get_toc_html(), + "raw": doc.html, + "parents": [ + {"url": d.get_absolute_url(), "title": d.title} for d in doc.parents ], - 'translations': [ + "translations": [ { - 'language': t.language, - 'hrefLang': t.get_hreflang(available_locales), - 'localizedLanguage': _(settings.LOCALES[t.locale].english), - 'locale': t.locale, - 'url': t.get_absolute_url(), - 'title': t.title - } for t in other_translations + "language": t.language, + "hrefLang": t.get_hreflang(available_locales), + "localizedLanguage": _(settings.LOCALES[t.locale].english), + "locale": t.locale, + "url": t.get_absolute_url(), + "title": t.title, + } + for t in other_translations ], - 'lastModified': (doc.current_revision and - doc.current_revision.created.isoformat()), + "lastModified": ( + doc.current_revision and doc.current_revision.created.isoformat() + ), }, - 'redirectURL': None, + "redirectURL": None, } @@ -205,23 +208,23 @@ def whoami(request): user = request.user if user.is_authenticated: data = { - 'username': user.username, - 'timezone': user.timezone, - 'is_authenticated': True, - 'is_staff': user.is_staff, - 'is_superuser': user.is_superuser, - 'is_beta_tester': user.is_beta_tester, - 'avatar_url': get_avatar_url(user), + "username": user.username, + "timezone": user.timezone, + "is_authenticated": True, + "is_staff": user.is_staff, + "is_superuser": user.is_superuser, + "is_beta_tester": user.is_beta_tester, + "avatar_url": get_avatar_url(user), } else: data = { - 'username': None, - 'timezone': settings.TIME_ZONE, - 'is_authenticated': False, - 'is_staff': False, - 'is_superuser': False, - 'is_beta_tester': False, - 'avatar_url': None, + "username": None, + "timezone": settings.TIME_ZONE, + "is_authenticated": False, + "is_staff": False, + "is_superuser": False, + "is_beta_tester": False, + "avatar_url": None, } # Add waffle data to the dict we're going to be returning. @@ -234,10 +237,10 @@ def whoami(request): # objects will then become: # get_waffle_flag_model().get_all() # - data['waffle'] = { - 'flags': {f.name: f.is_active(request) for f in Flag.get_all()}, - 'switches': {s.name: s.is_active() for s in Switch.get_all()}, - 'samples': {s.name: s.is_active() for s in Sample.get_all()}, + data["waffle"] = { + "flags": {f.name: f.is_active(request) for f in Flag.get_all()}, + "switches": {s.name: s.is_active() for s in Switch.get_all()}, + "samples": {s.name: s.is_active() for s in Sample.get_all()}, } return JsonResponse(data) @@ -246,7 +249,7 @@ class APIDocumentSerializer(serializers.Serializer): title = serializers.CharField(read_only=True, max_length=255) slug = serializers.CharField(read_only=True, max_length=255) locale = serializers.CharField(read_only=True, max_length=7) - excerpt = serializers.ReadOnlyField(source='get_excerpt') + excerpt = serializers.ReadOnlyField(source="get_excerpt") class APILanguageFilterBackend(LanguageFilterBackend): @@ -259,13 +262,13 @@ class APILanguageFilterBackend(LanguageFilterBackend): """ def filter_queryset(self, request, queryset, view): - locale = request.GET.get('locale') or settings.LANGUAGE_CODE + locale = request.GET.get("locale") or settings.LANGUAGE_CODE if locale not in settings.ACCEPTED_LOCALES: - raise serializers.ValidationError( - {'error': 'Not a valid locale code'}) + raise serializers.ValidationError({"error": "Not a valid locale code"}) request.LANGUAGE_CODE = locale return super(APILanguageFilterBackend, self).filter_queryset( - request, queryset, view) + request, queryset, view + ) class APISearchQueryBackend(SearchQueryBackend): @@ -273,12 +276,12 @@ class APISearchQueryBackend(SearchQueryBackend): stink if the 'q' query parameter is falsy.""" def filter_queryset(self, request, queryset, view): - search_term = (view.query_params.get('q') or '').strip() + search_term = (view.query_params.get("q") or "").strip() if not search_term: - raise serializers.ValidationError( - {'error': "Search term 'q' must be set"}) + raise serializers.ValidationError({"error": "Search term 'q' must be set"}) return super(APISearchQueryBackend, self).filter_queryset( - request, queryset, view) + request, queryset, view + ) class APISearchView(SearchView): @@ -296,8 +299,8 @@ class APISearchView(SearchView): search = never_cache(APISearchView.as_view()) -@ratelimit(key='user_or_ip', rate='10/d', block=True) -@api_view(['POST']) +@ratelimit(key="user_or_ip", rate="10/d", block=True) +@api_view(["POST"]) def bc_signal(request): if not settings.ENABLE_BCD_SIGNAL: return Response("not enabled", status=status.HTTP_400_BAD_REQUEST) @@ -318,12 +321,19 @@ def get_user(request, username): 404. The case of the username is not important, since the collation of the username column of the user table in MySQL is case-insensitive. """ - fields = ('username', 'title', 'fullname', 'organization', 'location', - 'timezone', 'locale') + fields = ( + "username", + "title", + "fullname", + "organization", + "location", + "timezone", + "locale", + ) try: user = User.objects.only(*fields).get(username=username) except User.DoesNotExist: raise Http404(f'No user exists with the username "{username}".') data = {field: getattr(user, field) for field in fields} - data['avatar_url'] = get_avatar_url(user) + data["avatar_url"] = get_avatar_url(user) return JsonResponse(data) diff --git a/kuma/attachments/admin.py b/kuma/attachments/admin.py index f3477e7b47f..4e06c287d85 100644 --- a/kuma/attachments/admin.py +++ b/kuma/attachments/admin.py @@ -16,35 +16,34 @@ class AttachmentRevisionInline(admin.StackedInline): model = AttachmentRevision extra = 1 can_delete = False - raw_id_fields = ['creator'] + raw_id_fields = ["creator"] form = AdminAttachmentRevisionForm @admin.register(Attachment) class AttachmentAdmin(DisabledDeleteActionMixin, admin.ModelAdmin): - fields = ['current_revision', 'mindtouch_attachment_id'] - list_display = ['id', 'title', 'modified', 'full_url'] - list_display_links = ['id', 'title'] + fields = ["current_revision", "mindtouch_attachment_id"] + list_display = ["id", "title", "modified", "full_url"] + list_display_links = ["id", "title"] list_filter = [ - 'modified', - 'current_revision__is_approved', - 'current_revision__mime_type', + "modified", + "current_revision__is_approved", + "current_revision__mime_type", ] - list_select_related = ['current_revision'] - ordering = ['-modified'] - search_fields = ['title'] - raw_id_fields = ['current_revision'] - date_hierarchy = 'modified' + list_select_related = ["current_revision"] + ordering = ["-modified"] + search_fields = ["title"] + raw_id_fields = ["current_revision"] + date_hierarchy = "modified" inlines = [AttachmentRevisionInline] def full_url(self, obj): url = obj.get_file_url() return format_html( - '{}', - url, - url + '{}', url, url ) - full_url.short_description = 'Full URL' + + full_url.short_description = "Full URL" def delete_revisions(self, request, revisions): # go through all revisions and trash them, @@ -52,20 +51,24 @@ def delete_revisions(self, request, revisions): trashed_attachments = [] for revision in revisions: trashed_attachment = revision.delete( - username=request.user.username, - individual=False, + username=request.user.username, individual=False, ) trashed_attachments.append(trashed_attachment) if trashed_attachments: self.message_user( request, - _('The following attachment files were moved to the trash: ' - '%(filenames)s. You may want to review them before their ' - 'automatic purge after %(days)s days from the file ' - 'storage.') % - {'filenames': get_text_list(trashed_attachments, _('and')), - 'days': config.WIKI_ATTACHMENTS_KEEP_TRASHED_DAYS}, - messages.SUCCESS) + _( + "The following attachment files were moved to the trash: " + "%(filenames)s. You may want to review them before their " + "automatic purge after %(days)s days from the file " + "storage." + ) + % { + "filenames": get_text_list(trashed_attachments, _("and")), + "days": config.WIKI_ATTACHMENTS_KEEP_TRASHED_DAYS, + }, + messages.SUCCESS, + ) return trashed_attachments def delete_model(self, request, obj): @@ -84,26 +87,31 @@ def save_formset(self, request, form, formset, change): @admin.register(AttachmentRevision) class AttachmentRevisionAdmin(DisabledDeleteActionMixin, admin.ModelAdmin): - fields = ['attachment', 'file', 'title', 'mime_type', 'description', - 'is_approved'] - list_display = ['id', 'title', 'created', 'mime_type', 'is_approved', - 'attachment_url'] - list_display_links = ['id', 'title'] - list_editable = ['is_approved'] - list_filter = ['created', 'is_approved', 'mime_type'] - ordering = ['-created'] - search_fields = ['title', 'description', 'creator__username'] - raw_id_fields = ['attachment'] - date_hierarchy = 'created' - list_select_related = ['creator'] + fields = ["attachment", "file", "title", "mime_type", "description", "is_approved"] + list_display = [ + "id", + "title", + "created", + "mime_type", + "is_approved", + "attachment_url", + ] + list_display_links = ["id", "title"] + list_editable = ["is_approved"] + list_filter = ["created", "is_approved", "mime_type"] + ordering = ["-created"] + search_fields = ["title", "description", "creator__username"] + raw_id_fields = ["attachment"] + date_hierarchy = "created" + list_select_related = ["creator"] form = AdminAttachmentRevisionForm def attachment_url(self, obj): attachment = obj.attachment - url = reverse('admin:attachments_attachment_change', - args=(attachment.pk,)) + url = reverse("admin:attachments_attachment_change", args=(attachment.pk,)) return format_html('{}', url, attachment.pk) - attachment_url.short_description = 'Attachment' + + attachment_url.short_description = "Attachment" def save_model(self, request, obj, form, change): obj.creator = request.user @@ -115,8 +123,9 @@ def has_delete_permission(self, request, obj=None): False for the permission check. """ if obj is None: - return super(AttachmentRevisionAdmin, - self).has_delete_permission(request, obj) + return super(AttachmentRevisionAdmin, self).has_delete_permission( + request, obj + ) else: return obj.siblings().count() != 0 @@ -126,18 +135,23 @@ def delete_model(self, request, obj): trash_item = obj.delete(username=request.user.username) self.message_user( request, - _('The attachment file "%(filename)s" was moved to the trash. ' - 'You may want to review the file before its automatic purge ' - 'after %(days)s days from the file storage system.') % { - 'filename': force_text(trash_item.filename), - 'days': config.WIKI_ATTACHMENTS_KEEP_TRASHED_DAYS, - }, messages.SUCCESS) + _( + 'The attachment file "%(filename)s" was moved to the trash. ' + "You may want to review the file before its automatic purge " + "after %(days)s days from the file storage system." + ) + % { + "filename": force_text(trash_item.filename), + "days": config.WIKI_ATTACHMENTS_KEEP_TRASHED_DAYS, + }, + messages.SUCCESS, + ) @admin.register(TrashedAttachment) class TrashedAttachmentAdmin(DisabledDeleteActionMixin, admin.ModelAdmin): - list_display = ['file', 'trashed_at', 'trashed_by', 'was_current'] - list_filter = ['trashed_at', 'was_current'] - search_fields = ['file'] - date_hierarchy = 'trashed_at' - readonly_fields = ['file', 'trashed_at', 'trashed_by', 'was_current'] + list_display = ["file", "trashed_at", "trashed_by", "was_current"] + list_filter = ["trashed_at", "was_current"] + search_fields = ["file"] + date_hierarchy = "trashed_at" + readonly_fields = ["file", "trashed_at", "trashed_by", "was_current"] diff --git a/kuma/attachments/apps.py b/kuma/attachments/apps.py index bd4a6c16934..2e3234d488b 100644 --- a/kuma/attachments/apps.py +++ b/kuma/attachments/apps.py @@ -7,8 +7,9 @@ class AttachmentsConfig(AppConfig): The Django App Config class to store information about the users app and do startup time things. """ - name = 'kuma.attachments' - verbose_name = _('Attachments') + + name = "kuma.attachments" + verbose_name = _("Attachments") def ready(self): # Register signal handlers diff --git a/kuma/attachments/feeds.py b/kuma/attachments/feeds.py index 614e0958b84..a774345d374 100644 --- a/kuma/attachments/feeds.py +++ b/kuma/attachments/feeds.py @@ -10,18 +10,18 @@ class AttachmentsFeed(DocumentsFeed): subtitle = _("Recent revisions to MDN file attachments") def items(self): - return (AttachmentRevision.objects.prefetch_related('creator', - 'attachment') - .order_by('-created')[:50]) + return AttachmentRevision.objects.prefetch_related( + "creator", "attachment" + ).order_by("-created")[:50] def item_title(self, item): return item.title def item_description(self, item): if item.get_previous() is None: - return f'

Created by: {item.creator.username}

' + return f"

Created by: {item.creator.username}

" else: - return f'

Edited by {item.creator.username} {item.comment}' + return f"

Edited by {item.creator.username} {item.comment}" def item_link(self, item): return item.attachment.get_file_url() diff --git a/kuma/attachments/forms.py b/kuma/attachments/forms.py index d5eb57355e9..3238bebc223 100644 --- a/kuma/attachments/forms.py +++ b/kuma/attachments/forms.py @@ -7,7 +7,7 @@ from .models import AttachmentRevision -MIME_TYPE_INVALID = _('Files of this type are not permitted.') +MIME_TYPE_INVALID = _("Files of this type are not permitted.") class AttachmentRevisionForm(forms.ModelForm): @@ -19,12 +19,13 @@ class AttachmentRevisionForm(forms.ModelForm): As a result of this, calling save(commit=True) is off-limits. """ + class Meta: model = AttachmentRevision - fields = ('file', 'title', 'description', 'comment') + fields = ("file", "title", "description", "comment") def __init__(self, *args, **kwargs): - self.allow_svg_uploads = kwargs.pop('allow_svg_uploads', False) + self.allow_svg_uploads = kwargs.pop("allow_svg_uploads", False) super(AttachmentRevisionForm, self).__init__(*args, **kwargs) self.mime_type = None @@ -38,23 +39,22 @@ def clean(self): """ cleaned_data = super(AttachmentRevisionForm, self).clean() nulls = EMPTY_VALUES + (AttachmentRevision.DEFAULT_MIME_TYPE,) - submitted_mime_type = cleaned_data.get('mime_type') + submitted_mime_type = cleaned_data.get("mime_type") - if submitted_mime_type in nulls and 'file' in cleaned_data: - self.mime_type = self.mime_type_from_file(cleaned_data['file']) - if self.mime_type.startswith('image/svg') and self.allow_svg_uploads: + if submitted_mime_type in nulls and "file" in cleaned_data: + self.mime_type = self.mime_type_from_file(cleaned_data["file"]) + if self.mime_type.startswith("image/svg") and self.allow_svg_uploads: # The `magic.Magic()` will, for unknown reasons, sometimes # think an SVG image's mime type is `image/svg` which not # a valid mime type actually. # See https://www.iana.org/assignments/media-types/media-types.xhtml#image # So correct that. - if self.mime_type == 'image/svg': - self.mime_type = 'image/svg+xml' + if self.mime_type == "image/svg": + self.mime_type = "image/svg+xml" else: allowed_mime_types = config.WIKI_ATTACHMENT_ALLOWED_TYPES.split() if self.mime_type not in allowed_mime_types: - raise forms.ValidationError( - MIME_TYPE_INVALID, code='invalid') + raise forms.ValidationError(MIME_TYPE_INVALID, code="invalid") return cleaned_data @@ -66,12 +66,18 @@ def save(self, *args, **kwargs): def mime_type_from_file(self, file): m_mime = magic.Magic(mime=True) - mime_type = m_mime.from_buffer(file.read(1024)).split(';')[0] + mime_type = m_mime.from_buffer(file.read(1024)).split(";")[0] file.seek(0) return mime_type class AdminAttachmentRevisionForm(AttachmentRevisionForm): class Meta(AttachmentRevisionForm.Meta): - fields = ['attachment', 'file', 'title', 'mime_type', 'description', - 'is_approved'] + fields = [ + "attachment", + "file", + "title", + "mime_type", + "description", + "is_approved", + ] diff --git a/kuma/attachments/management/commands/empty_attachments_trash.py b/kuma/attachments/management/commands/empty_attachments_trash.py index a748f670e92..28f3770d228 100644 --- a/kuma/attachments/management/commands/empty_attachments_trash.py +++ b/kuma/attachments/management/commands/empty_attachments_trash.py @@ -10,29 +10,37 @@ class Command(BaseCommand): help = "Empty the attachments trash" def add_arguments(self, parser): - parser.add_argument('-f', '--force', - action='store_true', dest='force', default=False, - help="Force deleting all trashed attachments.") - parser.add_argument('-n', '--dry-run', - action='store_true', dest='dry_run', default=False, - help="Do everything except actually emptying the " - "trash.") + parser.add_argument( + "-f", + "--force", + action="store_true", + dest="force", + default=False, + help="Force deleting all trashed attachments.", + ) + parser.add_argument( + "-n", + "--dry-run", + action="store_true", + dest="dry_run", + default=False, + help="Do everything except actually emptying the " "trash.", + ) def handle(self, *args, **options): - dry_run = options['dry_run'] + dry_run = options["dry_run"] - if options['force']: + if options["force"]: trashed_attachments = TrashedAttachment.objects.all() else: - timeframe = timedelta( - days=config.WIKI_ATTACHMENTS_KEEP_TRASHED_DAYS - ) + timeframe = timedelta(days=config.WIKI_ATTACHMENTS_KEEP_TRASHED_DAYS) trashed_attachments = TrashedAttachment.objects.filter( trashed_at__lte=date.today() - timeframe ) - self.stdout.write('Emptying attachments trash ' - 'for %s attachments...\n\n' % - trashed_attachments.approx_count()) + self.stdout.write( + "Emptying attachments trash " + "for %s attachments...\n\n" % trashed_attachments.approx_count() + ) deleted = [] # in case we have lots of attachments we don't want Django's @@ -44,10 +52,10 @@ def handle(self, *args, **options): if deleted: if dry_run: - self.stdout.write('Dry deleted the following files:') + self.stdout.write("Dry deleted the following files:") else: - self.stdout.write('Deleted the following files:') + self.stdout.write("Deleted the following files:") for deleted_item in deleted: - self.stdout.write('- %s' % deleted_item) + self.stdout.write("- %s" % deleted_item) else: - self.stdout.write('Nothing to delete!') + self.stdout.write("Nothing to delete!") diff --git a/kuma/attachments/migrations/0001_squashed_0008_attachment_on_delete.py b/kuma/attachments/migrations/0001_squashed_0008_attachment_on_delete.py index 02365ef4b9e..fc0ab7cea45 100644 --- a/kuma/attachments/migrations/0001_squashed_0008_attachment_on_delete.py +++ b/kuma/attachments/migrations/0001_squashed_0008_attachment_on_delete.py @@ -1,5 +1,3 @@ - - from django.db import migrations, models import datetime import kuma.attachments.utils @@ -15,75 +13,200 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Attachment', + name="Attachment", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('title', models.CharField(max_length=255, db_index=True)), - ('mindtouch_attachment_id', models.IntegerField(help_text=b'ID for migrated MindTouch resource', null=True, db_index=True)), - ('modified', models.DateTimeField(db_index=True, auto_now=True, null=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("title", models.CharField(max_length=255, db_index=True)), + ( + "mindtouch_attachment_id", + models.IntegerField( + help_text=b"ID for migrated MindTouch resource", + null=True, + db_index=True, + ), + ), + ( + "modified", + models.DateTimeField(db_index=True, auto_now=True, null=True), + ), ], options={ - 'permissions': (('disallow_add_attachment', 'Cannot upload attachment'),), + "permissions": ( + ("disallow_add_attachment", "Cannot upload attachment"), + ), }, ), migrations.CreateModel( - name='AttachmentRevision', + name="AttachmentRevision", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('file', models.FileField(max_length=500, upload_to=kuma.attachments.utils.attachment_upload_to)), - ('title', models.CharField(max_length=255, null=True, db_index=True)), - ('mime_type', models.CharField(max_length=255, db_index=True)), - ('description', models.TextField(blank=True)), - ('created', models.DateTimeField(default=datetime.datetime.now)), - ('comment', models.CharField(max_length=255, blank=True)), - ('is_approved', models.BooleanField(default=True, db_index=True)), - ('mindtouch_old_id', models.IntegerField(help_text=b'ID for migrated MindTouch resource revision', unique=True, null=True, db_index=True)), - ('is_mindtouch_migration', models.BooleanField(default=False, help_text=b'Did this revision come from MindTouch?', db_index=True)), - ('attachment', models.ForeignKey(related_name='revisions', to='attachments.Attachment', on_delete=models.CASCADE)), - ('creator', models.ForeignKey(related_name='created_attachment_revisions', to=settings.AUTH_USER_MODEL, on_delete=models.PROTECT)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "file", + models.FileField( + max_length=500, + upload_to=kuma.attachments.utils.attachment_upload_to, + ), + ), + ("title", models.CharField(max_length=255, null=True, db_index=True)), + ("mime_type", models.CharField(max_length=255, db_index=True)), + ("description", models.TextField(blank=True)), + ("created", models.DateTimeField(default=datetime.datetime.now)), + ("comment", models.CharField(max_length=255, blank=True)), + ("is_approved", models.BooleanField(default=True, db_index=True)), + ( + "mindtouch_old_id", + models.IntegerField( + help_text=b"ID for migrated MindTouch resource revision", + unique=True, + null=True, + db_index=True, + ), + ), + ( + "is_mindtouch_migration", + models.BooleanField( + default=False, + help_text=b"Did this revision come from MindTouch?", + db_index=True, + ), + ), + ( + "attachment", + models.ForeignKey( + related_name="revisions", + to="attachments.Attachment", + on_delete=models.CASCADE, + ), + ), + ( + "creator", + models.ForeignKey( + related_name="created_attachment_revisions", + to=settings.AUTH_USER_MODEL, + on_delete=models.PROTECT, + ), + ), ], ), migrations.AddField( - model_name='attachment', - name='current_revision', - field=models.ForeignKey(related_name='current_for+', on_delete=django.db.models.deletion.SET_NULL, blank=True, to='attachments.AttachmentRevision', null=True), + model_name="attachment", + name="current_revision", + field=models.ForeignKey( + related_name="current_for+", + on_delete=django.db.models.deletion.SET_NULL, + blank=True, + to="attachments.AttachmentRevision", + null=True, + ), ), migrations.AlterModelOptions( - name='attachmentrevision', - options={'verbose_name': 'attachment revision', 'verbose_name_plural': 'attachment revisions'}, + name="attachmentrevision", + options={ + "verbose_name": "attachment revision", + "verbose_name_plural": "attachment revisions", + }, ), migrations.AlterField( - model_name='attachment', - name='mindtouch_attachment_id', - field=models.IntegerField(help_text=b'ID for migrated MindTouch resource', null=True, db_index=True, blank=True), + model_name="attachment", + name="mindtouch_attachment_id", + field=models.IntegerField( + help_text=b"ID for migrated MindTouch resource", + null=True, + db_index=True, + blank=True, + ), ), migrations.AlterField( - model_name='attachmentrevision', - name='mindtouch_old_id', - field=models.IntegerField(help_text=b'ID for migrated MindTouch resource revision', unique=True, null=True, db_index=True, blank=True), + model_name="attachmentrevision", + name="mindtouch_old_id", + field=models.IntegerField( + help_text=b"ID for migrated MindTouch resource revision", + unique=True, + null=True, + db_index=True, + blank=True, + ), ), migrations.AlterField( - model_name='attachmentrevision', - name='mime_type', - field=models.CharField(default=b'application/octet-stream', max_length=255, db_index=True), + model_name="attachmentrevision", + name="mime_type", + field=models.CharField( + default=b"application/octet-stream", max_length=255, db_index=True + ), ), migrations.AlterField( - model_name='attachmentrevision', - name='mime_type', - field=models.CharField(default=b'application/octet-stream', help_text='The MIME type is used when serving the attachment. Automatically populated by inspecting the file on upload. Please only override if needed.', max_length=255, db_index=True, blank=True), + model_name="attachmentrevision", + name="mime_type", + field=models.CharField( + default=b"application/octet-stream", + help_text="The MIME type is used when serving the attachment. Automatically populated by inspecting the file on upload. Please only override if needed.", + max_length=255, + db_index=True, + blank=True, + ), ), migrations.CreateModel( - name='TrashedAttachment', + name="TrashedAttachment", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('file', models.FileField(help_text='The attachment file that was trashed', max_length=500, upload_to=kuma.attachments.utils.attachment_upload_to)), - ('trashed_at', models.DateTimeField(default=datetime.datetime.now, help_text='The date and time the attachment was trashed')), - ('trashed_by', models.CharField(help_text='The username of the user who trashed the attachment', max_length=30, blank=True)), - ('was_current', models.BooleanField(default=False, help_text='Whether or not this attachment was the current attachment revision at the time of trashing.')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "file", + models.FileField( + help_text="The attachment file that was trashed", + max_length=500, + upload_to=kuma.attachments.utils.attachment_upload_to, + ), + ), + ( + "trashed_at", + models.DateTimeField( + default=datetime.datetime.now, + help_text="The date and time the attachment was trashed", + ), + ), + ( + "trashed_by", + models.CharField( + help_text="The username of the user who trashed the attachment", + max_length=30, + blank=True, + ), + ), + ( + "was_current", + models.BooleanField( + default=False, + help_text="Whether or not this attachment was the current attachment revision at the time of trashing.", + ), + ), ], options={ - 'verbose_name': 'Trashed attachment', - 'verbose_name_plural': 'Trashed attachments', + "verbose_name": "Trashed attachment", + "verbose_name_plural": "Trashed attachments", }, ), ] diff --git a/kuma/attachments/migrations/0002_auto_20191023_0405.py b/kuma/attachments/migrations/0002_auto_20191023_0405.py index 701be47b1d4..a838a114dec 100644 --- a/kuma/attachments/migrations/0002_auto_20191023_0405.py +++ b/kuma/attachments/migrations/0002_auto_20191023_0405.py @@ -7,28 +7,49 @@ class Migration(migrations.Migration): dependencies = [ - ('attachments', '0001_squashed_0008_attachment_on_delete'), + ("attachments", "0001_squashed_0008_attachment_on_delete"), ] operations = [ migrations.AlterField( - model_name='attachment', - name='mindtouch_attachment_id', - field=models.IntegerField(blank=True, db_index=True, help_text='ID for migrated MindTouch resource', null=True), + model_name="attachment", + name="mindtouch_attachment_id", + field=models.IntegerField( + blank=True, + db_index=True, + help_text="ID for migrated MindTouch resource", + null=True, + ), ), migrations.AlterField( - model_name='attachmentrevision', - name='is_mindtouch_migration', - field=models.BooleanField(db_index=True, default=False, help_text='Did this revision come from MindTouch?'), + model_name="attachmentrevision", + name="is_mindtouch_migration", + field=models.BooleanField( + db_index=True, + default=False, + help_text="Did this revision come from MindTouch?", + ), ), migrations.AlterField( - model_name='attachmentrevision', - name='mime_type', - field=models.CharField(blank=True, db_index=True, default='application/octet-stream', help_text='The MIME type is used when serving the attachment. Automatically populated by inspecting the file on upload. Please only override if needed.', max_length=255), + model_name="attachmentrevision", + name="mime_type", + field=models.CharField( + blank=True, + db_index=True, + default="application/octet-stream", + help_text="The MIME type is used when serving the attachment. Automatically populated by inspecting the file on upload. Please only override if needed.", + max_length=255, + ), ), migrations.AlterField( - model_name='attachmentrevision', - name='mindtouch_old_id', - field=models.IntegerField(blank=True, db_index=True, help_text='ID for migrated MindTouch resource revision', null=True, unique=True), + model_name="attachmentrevision", + name="mindtouch_old_id", + field=models.IntegerField( + blank=True, + db_index=True, + help_text="ID for migrated MindTouch resource revision", + null=True, + unique=True, + ), ), ] diff --git a/kuma/attachments/migrations/0003_auto_20191016_storage.py b/kuma/attachments/migrations/0003_auto_20191016_storage.py index 861ddcf03cf..c6c33b78766 100644 --- a/kuma/attachments/migrations/0003_auto_20191016_storage.py +++ b/kuma/attachments/migrations/0003_auto_20191016_storage.py @@ -7,18 +7,27 @@ class Migration(migrations.Migration): dependencies = [ - ('attachments', '0002_auto_20191023_0405'), + ("attachments", "0002_auto_20191023_0405"), ] operations = [ migrations.AlterField( - model_name='attachmentrevision', - name='file', - field=models.FileField(max_length=500, storage=kuma.attachments.models.AttachmentStorage(), upload_to=kuma.attachments.utils.attachment_upload_to), + model_name="attachmentrevision", + name="file", + field=models.FileField( + max_length=500, + storage=kuma.attachments.models.AttachmentStorage(), + upload_to=kuma.attachments.utils.attachment_upload_to, + ), ), migrations.AlterField( - model_name='trashedattachment', - name='file', - field=models.FileField(help_text='The attachment file that was trashed', max_length=500, storage=kuma.attachments.models.AttachmentStorage(), upload_to=kuma.attachments.utils.attachment_upload_to), + model_name="trashedattachment", + name="file", + field=models.FileField( + help_text="The attachment file that was trashed", + max_length=500, + storage=kuma.attachments.models.AttachmentStorage(), + upload_to=kuma.attachments.utils.attachment_upload_to, + ), ), ] diff --git a/kuma/attachments/models.py b/kuma/attachments/models.py index 602e010f755..3a878389cb2 100644 --- a/kuma/attachments/models.py +++ b/kuma/attachments/models.py @@ -18,10 +18,8 @@ def __init__(self, *args, **kwargs): access_key=settings.ATTACHMENTS_AWS_ACCESS_KEY_ID, secret_key=settings.ATTACHMENTS_AWS_SECRET_ACCESS_KEY, bucket_name=settings.ATTACHMENTS_AWS_STORAGE_BUCKET_NAME, - object_parameters={ - 'CacheControl': 'public, max-age=31536000, immutable', - }, - default_acl='public-read', + object_parameters={"CacheControl": "public, max-age=31536000, immutable"}, + default_acl="public-read", querystring_auth=False, custom_domain=settings.ATTACHMENTS_AWS_S3_CUSTOM_DOMAIN, secure_urls=settings.ATTACHMENTS_AWS_S3_SECURE_URLS, @@ -44,11 +42,12 @@ class Attachment(models.Model): and documents; insertion of an attachment is handled through markup in the document. """ + current_revision = models.ForeignKey( - 'AttachmentRevision', + "AttachmentRevision", null=True, blank=True, - related_name='current_for+', + related_name="current_for+", on_delete=models.SET_NULL, ) # These get filled from the current revision. @@ -60,13 +59,14 @@ class Attachment(models.Model): # new kuma file URLs. mindtouch_attachment_id = models.IntegerField( help_text="ID for migrated MindTouch resource", - null=True, blank=True, db_index=True) + null=True, + blank=True, + db_index=True, + ) modified = models.DateTimeField(auto_now=True, null=True, db_index=True) class Meta(object): - permissions = ( - ("disallow_add_attachment", "Cannot upload attachment"), - ) + permissions = (("disallow_add_attachment", "Cannot upload attachment"),) def __str__(self): return self.title @@ -99,9 +99,7 @@ def attach(self, document, user, revision): # for the current attachment, a.k.a. this was a previous uploaded file DocumentAttachment = document.files.through try: - document_attachment = DocumentAttachment.objects.get( - file_id=self.pk - ) + document_attachment = DocumentAttachment.objects.get(file_id=self.pk) except document.files.through.DoesNotExist: # no previous uploads found, create a new document-attachment document_attachment = DocumentAttachment.objects.create( @@ -123,15 +121,15 @@ class AttachmentRevision(models.Model): """ A revision of an attachment. """ - DEFAULT_MIME_TYPE = 'application/octet-stream' - attachment = models.ForeignKey(Attachment, related_name='revisions', - on_delete=models.CASCADE) + DEFAULT_MIME_TYPE = "application/octet-stream" + + attachment = models.ForeignKey( + Attachment, related_name="revisions", on_delete=models.CASCADE + ) file = models.FileField( - storage=storage, - upload_to=attachment_upload_to, - max_length=500, + storage=storage, upload_to=attachment_upload_to, max_length=500, ) title = models.CharField(max_length=255, null=True, db_index=True) @@ -141,9 +139,11 @@ class AttachmentRevision(models.Model): db_index=True, blank=True, default=DEFAULT_MIME_TYPE, - help_text=_('The MIME type is used when serving the attachment. ' - 'Automatically populated by inspecting the file on ' - 'upload. Please only override if needed.'), + help_text=_( + "The MIME type is used when serving the attachment. " + "Automatically populated by inspecting the file on " + "upload. Please only override if needed." + ), ) # Does not allow wiki markup description = models.TextField(blank=True) @@ -152,7 +152,7 @@ class AttachmentRevision(models.Model): comment = models.CharField(max_length=255, blank=True) creator = models.ForeignKey( settings.AUTH_USER_MODEL, - related_name='created_attachment_revisions', + related_name="created_attachment_revisions", on_delete=models.PROTECT, ) is_approved = models.BooleanField(default=True, db_index=True) @@ -164,18 +164,21 @@ class AttachmentRevision(models.Model): # MindTouch? mindtouch_old_id = models.IntegerField( help_text="ID for migrated MindTouch resource revision", - null=True, blank=True, db_index=True, unique=True) + null=True, + blank=True, + db_index=True, + unique=True, + ) is_mindtouch_migration = models.BooleanField( - default=False, db_index=True, - help_text="Did this revision come from MindTouch?") + default=False, db_index=True, help_text="Did this revision come from MindTouch?" + ) class Meta: - verbose_name = _('attachment revision') - verbose_name_plural = _('attachment revisions') + verbose_name = _("attachment revision") + verbose_name_plural = _("attachment revisions") def __str__(self): - return ('%s (file: "%s", ID: #%s)' % - (self.title, self.filename, self.pk)) + return '%s (file: "%s", ID: #%s)' % (self.title, self.filename, self.pk) @property def filename(self): @@ -184,8 +187,9 @@ def filename(self): def save(self, *args, **kwargs): super(AttachmentRevision, self).save(*args, **kwargs) if self.is_approved and ( - not self.attachment.current_revision or - self.attachment.current_revision.id < self.id): + not self.attachment.current_revision + or self.attachment.current_revision.id < self.id + ): self.make_current() def delete(self, username=None, individual=True, *args, **kwargs): @@ -198,8 +202,10 @@ def delete(self, username=None, individual=True, *args, **kwargs): therefor the check if there are other sibling revisions is moot. """ if individual and self.siblings().count() == 0: - raise IntegrityError('You cannot delete the last revision of ' - 'attachment %s' % self.attachment) + raise IntegrityError( + "You cannot delete the last revision of " + "attachment %s" % self.attachment + ) trash_item = self.trash(username=username) super(AttachmentRevision, self).delete(*args, **kwargs) return trash_item @@ -213,12 +219,12 @@ def trash(self, username=None): """ trashed_attachment = TrashedAttachment( file=self.file, - trashed_by=username or 'unknown', + trashed_by=username or "unknown", was_current=( - self.attachment and - self.attachment.current_revision and - self.attachment.current_revision.pk == self.pk - ) + self.attachment + and self.attachment.current_revision + and self.attachment.current_revision.pk == self.pk + ), ) trashed_attachment.save() return trashed_attachment @@ -230,10 +236,13 @@ def make_current(self): self.attachment.save() def get_previous(self): - return self.attachment.revisions.filter( - is_approved=True, - created__lt=self.created, - ).order_by('-created').first() + return ( + self.attachment.revisions.filter( + is_approved=True, created__lt=self.created, + ) + .order_by("-created") + .first() + ) def siblings(self): return self.attachment.revisions.exclude(pk=self.pk) @@ -245,28 +254,30 @@ class TrashedAttachment(MySQLModel): storage=storage, upload_to=attachment_upload_to, max_length=500, - help_text=_('The attachment file that was trashed'), + help_text=_("The attachment file that was trashed"), ) trashed_at = models.DateTimeField( default=datetime.now, - help_text=_('The date and time the attachment was trashed'), + help_text=_("The date and time the attachment was trashed"), ) trashed_by = models.CharField( max_length=30, blank=True, - help_text=_('The username of the user who trashed the attachment'), + help_text=_("The username of the user who trashed the attachment"), ) was_current = models.BooleanField( default=False, - help_text=_('Whether or not this attachment was the current ' - 'attachment revision at the time of trashing.'), + help_text=_( + "Whether or not this attachment was the current " + "attachment revision at the time of trashing." + ), ) class Meta: - verbose_name = _('Trashed attachment') - verbose_name_plural = _('Trashed attachments') + verbose_name = _("Trashed attachment") + verbose_name_plural = _("Trashed attachments") def __str__(self): return self.filename diff --git a/kuma/attachments/signal_handlers.py b/kuma/attachments/signal_handlers.py index d97c2f112d6..0dddb1adf2c 100644 --- a/kuma/attachments/signal_handlers.py +++ b/kuma/attachments/signal_handlers.py @@ -4,8 +4,9 @@ from .models import AttachmentRevision, TrashedAttachment -@receiver(post_delete, sender=AttachmentRevision, - dispatch_uid='attachments.revision.delete') +@receiver( + post_delete, sender=AttachmentRevision, dispatch_uid="attachments.revision.delete" +) def after_revision_delete(instance, **kwargs): """ Signal handler to be called when an attachment revision is deleted @@ -17,8 +18,7 @@ def after_revision_delete(instance, **kwargs): previous.make_current() -@receiver(pre_delete, sender=TrashedAttachment, - dispatch_uid='attachments.trash.delete') +@receiver(pre_delete, sender=TrashedAttachment, dispatch_uid="attachments.trash.delete") def on_trash_delete(instance, **kwargs): """ Signal handler to be called when a trash item is deleted. diff --git a/kuma/attachments/tests/__init__.py b/kuma/attachments/tests/__init__.py index bee50785733..ac398eaa159 100644 --- a/kuma/attachments/tests/__init__.py +++ b/kuma/attachments/tests/__init__.py @@ -1,12 +1,12 @@ from django.core.files import temp as tempfile -def make_test_file(content=None, suffix='.txt'): +def make_test_file(content=None, suffix=".txt"): """ Create a fake file for testing purposes. """ if content is None: - content = 'I am a test file for upload.' + content = "I am a test file for upload." # Shamelessly stolen from Django's own file-upload tests. tdir = tempfile.gettempdir() file_for_upload = tempfile.NamedTemporaryFile(suffix=suffix, dir=tdir) diff --git a/kuma/attachments/tests/conftest.py b/kuma/attachments/tests/conftest.py index 5dc41c625eb..c978fcc10e5 100644 --- a/kuma/attachments/tests/conftest.py +++ b/kuma/attachments/tests/conftest.py @@ -9,8 +9,8 @@ @pytest.fixture def file_attachment(db, wiki_user): file_id = 97 - filename = 'test.txt' - title = 'Test text file' + filename = "test.txt" + title = "Test text file" attachment = Attachment(title=title, mindtouch_attachment_id=file_id) attachment.save() @@ -18,14 +18,11 @@ def file_attachment(db, wiki_user): title=title, is_approved=True, attachment=attachment, - mime_type='text/plain', - description='Initial upload', + mime_type="text/plain", + description="Initial upload", created=datetime.datetime.now(), ) revision.creator = wiki_user - revision.file.save(filename, ContentFile(b'This is only a test.')) + revision.file.save(filename, ContentFile(b"This is only a test.")) revision.make_current() - return dict( - attachment=attachment, - file=dict(id=file_id, name=filename), - ) + return dict(attachment=attachment, file=dict(id=file_id, name=filename),) diff --git a/kuma/attachments/tests/test_models.py b/kuma/attachments/tests/test_models.py index a0a1ac2936f..af30d8c6a25 100644 --- a/kuma/attachments/tests/test_models.py +++ b/kuma/attachments/tests/test_models.py @@ -15,33 +15,36 @@ class AttachmentModelTests(UserTestCase): - def setUp(self): super(AttachmentModelTests, self).setUp() - self.test_user = self.user_model.objects.get(username='testuser2') - self.attachment = Attachment(title='some title') + self.test_user = self.user_model.objects.get(username="testuser2") + self.attachment = Attachment(title="some title") self.attachment.save() self.revision = AttachmentRevision( attachment=self.attachment, - mime_type='text/plain', + mime_type="text/plain", title=self.attachment.title, - description='some description', + description="some description", created=datetime.datetime.now(), - is_approved=True) + is_approved=True, + ) self.revision.creator = self.test_user - self.revision.file.save('filename.txt', - ContentFile(b'Meh meh I am a test file.')) + self.revision.file.save( + "filename.txt", ContentFile(b"Meh meh I am a test file.") + ) self.revision2 = AttachmentRevision( attachment=self.attachment, - mime_type='text/plain', + mime_type="text/plain", title=self.attachment.title, - description='some description', + description="some description", created=datetime.datetime.now(), - is_approved=True) + is_approved=True, + ) self.revision2.creator = self.test_user - self.revision2.file.save('filename2.txt', - ContentFile(b'Meh meh I am a test file.')) + self.revision2.file.save( + "filename2.txt", ContentFile(b"Meh meh I am a test file.") + ) self.storage = AttachmentRevision.file.field.storage def test_document_attachment(self): @@ -49,7 +52,8 @@ def test_document_attachment(self): self.assertEqual(DocumentAttachment.objects.count(), 0) document_attachment1 = self.attachment.attach( - doc, self.test_user, self.revision) + doc, self.test_user, self.revision + ) self.assertEqual(DocumentAttachment.objects.count(), 1) self.assertEqual(document_attachment1.file, self.attachment) self.assertTrue(document_attachment1.is_original) @@ -57,7 +61,8 @@ def test_document_attachment(self): self.assertEqual(document_attachment1.attached_by, self.test_user) document_attachment2 = self.attachment.attach( - doc, self.test_user, self.revision2) + doc, self.test_user, self.revision2 + ) self.assertEqual(DocumentAttachment.objects.count(), 1) self.assertEqual(document_attachment2.file, self.attachment) self.assertTrue(document_attachment2.is_original) @@ -69,20 +74,18 @@ def test_trash_revision(self): self.assertEqual(TrashedAttachment.objects.count(), 0) trashed_attachment = self.revision2.trash() self.assertEqual(TrashedAttachment.objects.count(), 1) - self.assertEqual(trashed_attachment.file, - self.attachment.current_revision.file) - self.assertEqual(trashed_attachment.trashed_by, 'unknown') + self.assertEqual(trashed_attachment.file, self.attachment.current_revision.file) + self.assertEqual(trashed_attachment.trashed_by, "unknown") self.assertTrue(trashed_attachment.was_current) # the attachment revision wasn't really deleted, # only a trash item created - self.assertTrue(AttachmentRevision.objects.filter(pk=self.revision.pk) - .exists()) + self.assertTrue(AttachmentRevision.objects.filter(pk=self.revision.pk).exists()) def test_trash_revision_with_username(self): self.assertTrue(self.storage.exists(self.revision.file.name)) - trashed_attachment = self.revision.trash(username='trasher') + trashed_attachment = self.revision.trash(username="trasher") self.assertEqual(TrashedAttachment.objects.count(), 1) - self.assertEqual(trashed_attachment.trashed_by, 'trasher') + self.assertEqual(trashed_attachment.trashed_by, "trasher") self.assertTrue(self.storage.exists(self.revision.file.name)) def test_delete_revision_directly(self): @@ -97,14 +100,14 @@ def test_delete_revision_directly(self): def test_first_trash_then_delete_revision(self): pk = self.revision.pk - trashed_attachment = self.revision.delete(username='trasher') + trashed_attachment = self.revision.delete(username="trasher") self.assertTrue(trashed_attachment) self.assertFalse(AttachmentRevision.objects.filter(pk=pk).exists()) def test_deleting_trashed_item(self): pk = self.revision2.pk path = self.revision2.file.name - trashed_attachment = self.revision2.delete(username='trasher') + trashed_attachment = self.revision2.delete(username="trasher") self.assertTrue(trashed_attachment) self.assertFalse(AttachmentRevision.objects.filter(pk=pk).exists()) self.assertTrue(self.storage.exists(path)) @@ -128,60 +131,57 @@ def test_permissions(self): attachments work. """ # Get the negative and positive permissions - ct = ContentType.objects.get(app_label='attachments', - model='attachment') - p1 = Permission.objects.get(codename='disallow_add_attachment', - content_type=ct) - p2 = Permission.objects.get(codename='add_attachment', - content_type=ct) + ct = ContentType.objects.get(app_label="attachments", model="attachment") + p1 = Permission.objects.get(codename="disallow_add_attachment", content_type=ct) + p2 = Permission.objects.get(codename="add_attachment", content_type=ct) # Create a group with the negative permission. - g1, created = Group.objects.get_or_create(name='cannot_attach') + g1, created = Group.objects.get_or_create(name="cannot_attach") g1.permissions.set([p1]) g1.save() # Create a group with the positive permission. - g2, created = Group.objects.get_or_create(name='can_attach') + g2, created = Group.objects.get_or_create(name="can_attach") g2.permissions.set([p2]) g2.save() # User with no explicit permission is allowed - u2 = user(username='test_user2', save=True) + u2 = user(username="test_user2", save=True) self.assertTrue(allow_add_attachment_by(u2)) # User in group with negative permission is disallowed - u3 = user(username='test_user3', save=True) + u3 = user(username="test_user3", save=True) u3.groups.set([g1]) u3.save() self.assertTrue(not allow_add_attachment_by(u3)) # Superusers can do anything, despite group perms - u1 = user(username='test_super', is_superuser=True, save=True) + u1 = user(username="test_super", is_superuser=True, save=True) u1.groups.set([g1]) u1.save() self.assertTrue(allow_add_attachment_by(u1)) # User with negative permission is disallowed - u4 = user(username='test_user4', save=True) + u4 = user(username="test_user4", save=True) u4.user_permissions.add(p1) u4.save() self.assertTrue(not allow_add_attachment_by(u4)) # User with positive permission overrides group - u5 = user(username='test_user5', save=True) + u5 = user(username="test_user5", save=True) u5.groups.set([g1]) u5.user_permissions.add(p2) u5.save() self.assertTrue(allow_add_attachment_by(u5)) # Group with positive permission takes priority - u6 = user(username='test_user6', save=True) + u6 = user(username="test_user6", save=True) u6.groups.set([g1, g2]) u6.save() self.assertTrue(allow_add_attachment_by(u6)) # positive permission takes priority, period. - u7 = user(username='test_user7', save=True) + u7 = user(username="test_user7", save=True) u7.user_permissions.add(p1) u7.user_permissions.add(p2) u7.save() @@ -190,5 +190,5 @@ def test_permissions(self): @override_config(WIKI_ATTACHMENTS_DISABLE_UPLOAD=True) def test_permissions_when_disabled(self): # All users, including superusers, are denied - admin = self.user_model.objects.get(username='admin') + admin = self.user_model.objects.get(username="admin") self.assertFalse(allow_add_attachment_by(admin)) diff --git a/kuma/attachments/tests/test_templates.py b/kuma/attachments/tests/test_templates.py index f200a7aae3b..3f31c220478 100644 --- a/kuma/attachments/tests/test_templates.py +++ b/kuma/attachments/tests/test_templates.py @@ -10,40 +10,43 @@ @pytest.mark.security -def test_xss_file_attachment_title(admin_client, constance_config, root_doc, - wiki_user, editor_client, settings): - constance_config.WIKI_ATTACHMENT_ALLOWED_TYPES = 'text/plain' +def test_xss_file_attachment_title( + admin_client, constance_config, root_doc, wiki_user, editor_client, settings +): + constance_config.WIKI_ATTACHMENT_ALLOWED_TYPES = "text/plain" # use view to create new attachment file_for_upload = make_test_file() - files_url = reverse('attachments.edit_attachment', - kwargs={'document_path': root_doc.slug}) + files_url = reverse( + "attachments.edit_attachment", kwargs={"document_path": root_doc.slug} + ) title = '">' post_data = { - 'title': title, - 'description': 'xss', - 'comment': 'xss', - 'file': file_for_upload, + "title": title, + "description": "xss", + "comment": "xss", + "file": file_for_upload, } - response = admin_client.post(files_url, data=post_data, - HTTP_HOST=settings.WIKI_HOST) + response = admin_client.post( + files_url, data=post_data, HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 302 # now stick it in/on a document attachment = Attachment.objects.get(title=title) content = '' % attachment.get_file_url() root_doc.current_revision = Revision.objects.create( - document=root_doc, creator=wiki_user, content=content) + document=root_doc, creator=wiki_user, content=content + ) # view it and verify markup is escaped - response = editor_client.get(root_doc.get_edit_url(), - HTTP_HOST=settings.WIKI_HOST) + response = editor_client.get(root_doc.get_edit_url(), HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 doc = pq(response.content) - text = doc('.page-attachments-table .attachment-name-cell').text() - assert text == ('%s\nxss' % title) - html = to_html(doc('.page-attachments-table .attachment-name-cell')) - assert '><img src=x onerror=prompt(navigator.userAgent);>' in html + text = doc(".page-attachments-table .attachment-name-cell").text() + assert text == ("%s\nxss" % title) + html = to_html(doc(".page-attachments-table .attachment-name-cell")) + assert "><img src=x onerror=prompt(navigator.userAgent);>" in html # security bug 1272791 - for script in doc('script'): + for script in doc("script"): assert title not in script.text_content() diff --git a/kuma/attachments/tests/test_views.py b/kuma/attachments/tests/test_views.py index ce0fd324b75..0dbd7c77535 100644 --- a/kuma/attachments/tests/test_views.py +++ b/kuma/attachments/tests/test_views.py @@ -9,8 +9,11 @@ from django.db import transaction from pyquery import PyQuery as pq -from kuma.core.tests import (assert_no_cache_header, assert_redirect_to_wiki, - assert_shared_cache_header) +from kuma.core.tests import ( + assert_no_cache_header, + assert_redirect_to_wiki, + assert_shared_cache_header, +) from kuma.core.urlresolvers import reverse from kuma.core.utils import to_html from kuma.users.tests import UserTestCase @@ -22,42 +25,42 @@ from ..utils import convert_to_http_date -@override_config(WIKI_ATTACHMENT_ALLOWED_TYPES='text/plain') +@override_config(WIKI_ATTACHMENT_ALLOWED_TYPES="text/plain") class AttachmentViewTests(UserTestCase, WikiTestCase): - def setUp(self): super(AttachmentViewTests, self).setUp() - self.client.login(username='admin', password='testpass') + self.client.login(username="admin", password="testpass") self.revision = revision(save=True) self.document = self.revision.document - self.files_url = reverse('attachments.edit_attachment', - kwargs={'document_path': self.document.slug}) + self.files_url = reverse( + "attachments.edit_attachment", kwargs={"document_path": self.document.slug} + ) @transaction.atomic def _post_attachment(self): - file_for_upload = make_test_file( - content='A test file uploaded into kuma.') + file_for_upload = make_test_file(content="A test file uploaded into kuma.") post_data = { - 'title': 'Test uploaded file', - 'description': 'A test file uploaded into kuma.', - 'comment': 'Initial upload', - 'file': file_for_upload, + "title": "Test uploaded file", + "description": "A test file uploaded into kuma.", + "comment": "Initial upload", + "file": file_for_upload, } - response = self.client.post(self.files_url, data=post_data, - HTTP_HOST=settings.WIKI_HOST) + response = self.client.post( + self.files_url, data=post_data, HTTP_HOST=settings.WIKI_HOST + ) return response def test_edit_attachment(self): response = self._post_attachment() assert_no_cache_header(response) assert response.status_code == 302 - assert response['Location'] == self.document.get_edit_url() + assert response["Location"] == self.document.get_edit_url() - attachment = Attachment.objects.get(title='Test uploaded file') + attachment = Attachment.objects.get(title="Test uploaded file") rev = attachment.current_revision - assert rev.creator.username == 'admin' - assert rev.description == 'A test file uploaded into kuma.' - assert rev.comment == 'Initial upload' + assert rev.creator.username == "admin" + assert rev.description == "A test file uploaded into kuma." + assert rev.comment == "Initial upload" assert rev.is_approved @override_config(WIKI_ATTACHMENTS_DISABLE_UPLOAD=True) @@ -66,63 +69,70 @@ def test_disabled_edit_attachment(self): assert_no_cache_header(response) self.assertEqual(response.status_code, 403) # HTTP 403 Forbidden with self.assertRaises(Attachment.DoesNotExist): - Attachment.objects.get(title='Test uploaded file') + Attachment.objects.get(title="Test uploaded file") def test_get_previous(self): """ AttachmentRevision.get_previous() should return this revisions's files's most recent approved revision.""" - test_user = self.user_model.objects.get(username='testuser2') - attachment = Attachment(title='Test attachment for get_previous') + test_user = self.user_model.objects.get(username="testuser2") + attachment = Attachment(title="Test attachment for get_previous") attachment.save() revision1 = AttachmentRevision( attachment=attachment, - mime_type='text/plain', + mime_type="text/plain", title=attachment.title, - description='', - comment='Initial revision.', + description="", + comment="Initial revision.", created=datetime.datetime.now() - datetime.timedelta(seconds=30), creator=test_user, - is_approved=True) - revision1.file.save('get_previous_test_file.txt', - ContentFile(b'I am a test file for get_previous')) + is_approved=True, + ) + revision1.file.save( + "get_previous_test_file.txt", + ContentFile(b"I am a test file for get_previous"), + ) revision1.save() revision1.make_current() revision2 = AttachmentRevision( attachment=attachment, - mime_type='text/plain', + mime_type="text/plain", title=attachment.title, - description='', - comment='First edit..', + description="", + comment="First edit..", created=datetime.datetime.now(), creator=test_user, - is_approved=True) - revision2.file.save('get_previous_test_file.txt', - ContentFile(b'I am a test file for get_previous')) + is_approved=True, + ) + revision2.file.save( + "get_previous_test_file.txt", + ContentFile(b"I am a test file for get_previous"), + ) revision2.save() revision2.make_current() assert revision1 == revision2.get_previous() - @override_config(WIKI_ATTACHMENT_ALLOWED_TYPES='application/x-super-weird') + @override_config(WIKI_ATTACHMENT_ALLOWED_TYPES="application/x-super-weird") def test_mime_type_filtering(self): """ Don't allow uploads outside of the explicitly-permitted mime-types. """ - _file = make_test_file(content='plain and text', suffix='.txt') + _file = make_test_file(content="plain and text", suffix=".txt") post_data = { - 'title': 'Test disallowed file type', - 'description': 'A file kuma should disallow on type.', - 'comment': 'Initial upload', - 'file': _file, + "title": "Test disallowed file type", + "description": "A file kuma should disallow on type.", + "comment": "Initial upload", + "file": _file, } - response = self.client.post(self.files_url, data=post_data, - HTTP_HOST=settings.WIKI_HOST) + response = self.client.post( + self.files_url, data=post_data, HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 200 assert_no_cache_header(response) - self.assertContains(response, 'Files of this type are not permitted.') + self.assertContains(response, "Files of this type are not permitted.") _file.close() def test_svg_mime_type_staff_override(self): @@ -130,40 +140,44 @@ def test_svg_mime_type_staff_override(self): Staff users are allowed to upload SVG images. Only. """ _file = make_test_file( - content='', suffix='.svg') + content='', suffix=".svg" + ) post_data = { - 'title': 'Test Svg upload', - 'description': 'Mime type only allowed for some users.', - 'comment': 'Initial upload', - 'file': _file, + "title": "Test Svg upload", + "description": "Mime type only allowed for some users.", + "comment": "Initial upload", + "file": _file, } # Remember, self.client use logged in as user 'admin' - response = self.client.post(self.files_url, data=post_data, - HTTP_HOST=settings.WIKI_HOST) + response = self.client.post( + self.files_url, data=post_data, HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 302 # means it worked assert_no_cache_header(response) _file.close() - attachment_revision, = AttachmentRevision.objects.all() - assert attachment_revision.mime_type == 'image/svg+xml' + (attachment_revision,) = AttachmentRevision.objects.all() + assert attachment_revision.mime_type == "image/svg+xml" def test_svg_mime_type_non_staff(self): """ Regular users are not allowed to upload SVG images. """ _file = make_test_file( - content='', suffix='.svg') + content='', suffix=".svg" + ) post_data = { - 'title': 'Test Svg upload', - 'description': 'Mime type only allowed for some users.', - 'comment': 'Initial upload', - 'file': _file, + "title": "Test Svg upload", + "description": "Mime type only allowed for some users.", + "comment": "Initial upload", + "file": _file, } - self.client.login(username='testuser', password='testpass') - response = self.client.post(self.files_url, data=post_data, - HTTP_HOST=settings.WIKI_HOST) + self.client.login(username="testuser", password="testpass") + response = self.client.post( + self.files_url, data=post_data, HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 200 # means it didn't upload assert_no_cache_header(response) - self.assertContains(response, 'Files of this type are not permitted.') + self.assertContains(response, "Files of this type are not permitted.") _file.close() def test_intermediate(self): @@ -172,24 +186,25 @@ def test_intermediate(self): correctly when adding an Attachment with a document_id. """ - doc = document(locale='en-US', - slug='attachment-test-intermediate', - save=True) + doc = document(locale="en-US", slug="attachment-test-intermediate", save=True) revision(document=doc, is_approved=True, save=True) file_for_upload = make_test_file( - content='A file for testing intermediate attachment model.') + content="A file for testing intermediate attachment model." + ) post_data = { - 'title': 'Intermediate test file', - 'description': 'Intermediate test file', - 'comment': 'Initial upload', - 'file': file_for_upload, + "title": "Intermediate test file", + "description": "Intermediate test file", + "comment": "Initial upload", + "file": file_for_upload, } - files_url = reverse('attachments.edit_attachment', - kwargs={'document_path': doc.slug}) - response = self.client.post(files_url, data=post_data, - HTTP_HOST=settings.WIKI_HOST) + files_url = reverse( + "attachments.edit_attachment", kwargs={"document_path": doc.slug} + ) + response = self.client.post( + files_url, data=post_data, HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 302 assert_no_cache_header(response) @@ -198,146 +213,150 @@ def test_intermediate(self): assert intermediates.count() == 1 intermediate = intermediates[0] - assert intermediate.attached_by.username == 'admin' - assert intermediate.name == file_for_upload.name.split('/')[-1] + assert intermediate.attached_by.username == "admin" + assert intermediate.name == file_for_upload.name.split("/")[-1] def test_feed(self): - test_user = self.user_model.objects.get(username='testuser2') - attachment = Attachment(title='Test attachment for get_previous') + test_user = self.user_model.objects.get(username="testuser2") + attachment = Attachment(title="Test attachment for get_previous") attachment.save() revision = AttachmentRevision( attachment=attachment, - mime_type='text/plain', + mime_type="text/plain", title=attachment.title, - description='', - comment='Initial revision.', + description="", + comment="Initial revision.", created=datetime.datetime.now() - datetime.timedelta(seconds=30), creator=test_user, - is_approved=True) - revision.file.save('get_previous_test_file.txt', - ContentFile(b'I am a test file for get_previous')) + is_approved=True, + ) + revision.file.save( + "get_previous_test_file.txt", + ContentFile(b"I am a test file for get_previous"), + ) revision.save() revision.make_current() - feed_url = reverse('attachments.feeds.recent_files', - kwargs={'format': 'json'}) + feed_url = reverse("attachments.feeds.recent_files", kwargs={"format": "json"}) response = self.client.get(feed_url) assert_shared_cache_header(response) data = json.loads(response.content) assert len(data) == 1 - assert data[0]['title'] == revision.title - assert data[0]['link'] == revision.attachment.get_file_url() - assert data[0]['author_name'] == test_user.username + assert data[0]["title"] == revision.title + assert data[0]["link"] == revision.attachment.get_file_url() + assert data[0]["author_name"] == test_user.username def test_legacy_redirect(client, file_attachment): mindtouch_url = reverse( - 'attachments.mindtouch_file_redirect', + "attachments.mindtouch_file_redirect", args=(), kwargs={ - 'file_id': file_attachment['file']['id'], - 'filename': file_attachment['file']['name'] - } + "file_id": file_attachment["file"]["id"], + "filename": file_attachment["file"]["name"], + }, ) response = client.get(mindtouch_url) assert response.status_code == 301 assert_shared_cache_header(response) - assert response['Location'] == file_attachment['attachment'].get_file_url() - assert not response.has_header('Vary') + assert response["Location"] == file_attachment["attachment"].get_file_url() + assert not response.has_header("Vary") def test_edit_attachment_get(admin_client, root_doc): url = reverse( - 'attachments.edit_attachment', - kwargs={'document_path': root_doc.slug}) + "attachments.edit_attachment", kwargs={"document_path": root_doc.slug} + ) response = admin_client.get(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 302 assert_no_cache_header(response) - assert urlparse(response['Location']).path == root_doc.get_edit_url() + assert urlparse(response["Location"]).path == root_doc.get_edit_url() -@pytest.mark.parametrize('mode', ['empty-file', 'no-file']) -def test_edit_attachment_post_with_vacant_file(admin_client, root_doc, tmpdir, - mode): +@pytest.mark.parametrize("mode", ["empty-file", "no-file"]) +def test_edit_attachment_post_with_vacant_file(admin_client, root_doc, tmpdir, mode): post_data = { - 'title': 'Test uploaded file', - 'description': 'A test file uploaded into kuma.', - 'comment': 'Initial upload', + "title": "Test uploaded file", + "description": "A test file uploaded into kuma.", + "comment": "Initial upload", } - if mode == 'empty-file': - empty_file = tmpdir.join('empty') - empty_file.write('') - post_data['file'] = empty_file - expected = 'The submitted file is empty.' + if mode == "empty-file": + empty_file = tmpdir.join("empty") + empty_file.write("") + post_data["file"] = empty_file + expected = "The submitted file is empty." else: - expected = 'This field is required.' + expected = "This field is required." - url = reverse('attachments.edit_attachment', - kwargs={'document_path': root_doc.slug}) - response = admin_client.post(url, data=post_data, - HTTP_HOST=settings.WIKI_HOST) + url = reverse( + "attachments.edit_attachment", kwargs={"document_path": root_doc.slug} + ) + response = admin_client.post(url, data=post_data, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 doc = pq(response.content) assert to_html(doc('ul.errorlist a[href="#id_file"]')) == expected def test_raw_file_requires_attachment_host(client, settings, file_attachment): - settings.ATTACHMENT_HOST = 'demos' - settings.ALLOWED_HOSTS.append('demos') - attachment = file_attachment['attachment'] + settings.ATTACHMENT_HOST = "demos" + settings.ALLOWED_HOSTS.append("demos") + attachment = file_attachment["attachment"] created = attachment.current_revision.created url = attachment.get_file_url() # Force the HOST header to look like something other than "demos". - response = client.get(url, HTTP_HOST='testserver') + response = client.get(url, HTTP_HOST="testserver") assert response.status_code == 301 - assert 'public' in response['Cache-Control'] - assert 'max-age=900' in response['Cache-Control'] - assert response['Location'] == url - assert 'Vary' not in response + assert "public" in response["Cache-Control"] + assert "max-age=900" in response["Cache-Control"] + assert response["Location"] == url + assert "Vary" not in response response = client.get(url, HTTP_HOST=settings.ATTACHMENT_HOST) if settings.ATTACHMENTS_USE_S3: # Figure out the external scheme + host for our attachments bucket endpoint_url = settings.ATTACHMENTS_AWS_S3_ENDPOINT_URL - custom_proto = "https" if settings.ATTACHMENTS_AWS_S3_SECURE_URLS else 'http' - custom_url = f'{custom_proto}://{settings.ATTACHMENTS_AWS_S3_CUSTOM_DOMAIN}' - bucket_url = custom_url if settings.ATTACHMENTS_AWS_S3_CUSTOM_DOMAIN else endpoint_url + custom_proto = "https" if settings.ATTACHMENTS_AWS_S3_SECURE_URLS else "http" + custom_url = f"{custom_proto}://{settings.ATTACHMENTS_AWS_S3_CUSTOM_DOMAIN}" + bucket_url = ( + custom_url if settings.ATTACHMENTS_AWS_S3_CUSTOM_DOMAIN else endpoint_url + ) # Verify we're redirecting to the intended bucket or custom frontend assert response.status_code == 302 - assert response['location'].startswith(bucket_url) + assert response["location"].startswith(bucket_url) else: assert response.status_code == 200 assert response.streaming - assert response['x-frame-options'] == f'ALLOW-FROM {settings.DOMAIN}' - assert response['Last-Modified'] == convert_to_http_date(created) - assert 'public' in response['Cache-Control'] - assert 'max-age=900' in response['Cache-Control'] + assert response["x-frame-options"] == f"ALLOW-FROM {settings.DOMAIN}" + assert response["Last-Modified"] == convert_to_http_date(created) + assert "public" in response["Cache-Control"] + assert "max-age=900" in response["Cache-Control"] def test_raw_file_if_modified_since(client, settings, file_attachment): - settings.ATTACHMENT_HOST = 'demos' - settings.ALLOWED_HOSTS.append('demos') - attachment = file_attachment['attachment'] + settings.ATTACHMENT_HOST = "demos" + settings.ALLOWED_HOSTS.append("demos") + attachment = file_attachment["attachment"] created = attachment.current_revision.created url = attachment.get_file_url() response = client.get( url, HTTP_HOST=settings.ATTACHMENT_HOST, - HTTP_IF_MODIFIED_SINCE=convert_to_http_date(created) + HTTP_IF_MODIFIED_SINCE=convert_to_http_date(created), ) assert response.status_code == 304 - assert response['Last-Modified'] == convert_to_http_date(created) - assert 'public' in response['Cache-Control'] - assert 'max-age=900' in response['Cache-Control'] + assert response["Last-Modified"] == convert_to_http_date(created) + assert "public" in response["Cache-Control"] + assert "max-age=900" in response["Cache-Control"] def test_edit_attachment_redirect(client, root_doc): - url = reverse('attachments.edit_attachment', - kwargs={'document_path': root_doc.slug}) + url = reverse( + "attachments.edit_attachment", kwargs={"document_path": root_doc.slug} + ) response = client.get(url) assert_redirect_to_wiki(response, url) diff --git a/kuma/attachments/urls.py b/kuma/attachments/urls.py index 37bdeb29b8f..53ee163fcd1 100644 --- a/kuma/attachments/urls.py +++ b/kuma/attachments/urls.py @@ -4,10 +4,14 @@ urlpatterns = [ - url(r'^files/(?P\d+)/(?P.+)$', + url( + r"^files/(?P\d+)/(?P.+)$", views.raw_file, - name='attachments.raw_file'), - url(r'^@api/deki/files/(?P\d+)/=(?P.+)$', + name="attachments.raw_file", + ), + url( + r"^@api/deki/files/(?P\d+)/=(?P.+)$", views.mindtouch_file_redirect, - name='attachments.mindtouch_file_redirect'), + name="attachments.mindtouch_file_redirect", + ), ] diff --git a/kuma/attachments/utils.py b/kuma/attachments/utils.py index 84e7e4b94fb..ae00274e5ad 100644 --- a/kuma/attachments/utils.py +++ b/kuma/attachments/utils.py @@ -23,10 +23,10 @@ def allow_add_attachment_by(user): if user.is_superuser or user.is_staff: # Superusers and staff always allowed return True - if user.has_perm('attachments.add_attachment'): + if user.has_perm("attachments.add_attachment"): # Explicit add permission overrides disallow return True - if user.has_perm('attachments.disallow_add_attachment'): + if user.has_perm("attachments.disallow_add_attachment"): # Disallow generally applied via group, so per-user allow can # override return False @@ -34,11 +34,11 @@ def allow_add_attachment_by(user): def full_attachment_url(attachment_id, filename): - path = reverse('attachments.raw_file', kwargs={ - 'attachment_id': attachment_id, - 'filename': filename, - }) - return f'{settings.PROTOCOL}{settings.ATTACHMENT_HOST}{path}' + path = reverse( + "attachments.raw_file", + kwargs={"attachment_id": attachment_id, "filename": filename}, + ) + return f"{settings.PROTOCOL}{settings.ATTACHMENT_HOST}{path}" def convert_to_utc(dt): @@ -79,8 +79,8 @@ def attachment_upload_to(instance, filename): # microsecond, of when the path is generated. now = datetime.now() return "attachments/%(date)s/%(id)s/%(md5)s/%(filename)s" % { - 'date': now.strftime('%Y/%m/%d'), - 'id': instance.attachment.id, - 'md5': hashlib.md5(str(now).encode()).hexdigest(), - 'filename': filename + "date": now.strftime("%Y/%m/%d"), + "id": instance.attachment.id, + "md5": hashlib.md5(str(now).encode()).hexdigest(), + "filename": filename, } diff --git a/kuma/attachments/views.py b/kuma/attachments/views.py index 91c8288ea3c..5dde688b81e 100644 --- a/kuma/attachments/views.py +++ b/kuma/attachments/views.py @@ -9,8 +9,11 @@ from django.views.decorators.cache import cache_control, never_cache from django.views.decorators.clickjacking import xframe_options_sameorigin -from kuma.core.decorators import (ensure_wiki_domain, login_required, - shared_cache_control) +from kuma.core.decorators import ( + ensure_wiki_domain, + login_required, + shared_cache_control, +) from kuma.core.utils import is_untrusted from kuma.wiki.decorators import process_document_path from kuma.wiki.models import Document @@ -22,11 +25,11 @@ # Mime types used on MDN OVERRIDE_MIMETYPES = { - 'image/jpeg': '.jpeg, .jpg, .jpe', - 'image/vnd.adobe.photoshop': '.psd', + "image/jpeg": ".jpeg, .jpg, .jpe", + "image/vnd.adobe.photoshop": ".psd", } -IMAGE_MIMETYPES = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif'] +IMAGE_MIMETYPES = ["image/png", "image/jpeg", "image/jpg", "image/gif"] def guess_extension(_type): @@ -38,7 +41,7 @@ def raw_file(request, attachment_id, filename): """ Serve up an attachment's file. """ - qs = Attachment.objects.select_related('current_revision') + qs = Attachment.objects.select_related("current_revision") attachment = get_object_or_404(qs, pk=attachment_id) rev = attachment.current_revision if rev is None: @@ -52,20 +55,22 @@ def raw_file(request, attachment_id, filename): # Very important while we're potentially serving attachments from disk. # Far less important when we're just redirecting to S3. # Consider removing? - if_modified_since = parse_http_date_safe(request.META.get('HTTP_IF_MODIFIED_SINCE')) - if if_modified_since and if_modified_since >= calendar.timegm(rev.created.utctimetuple()): + if_modified_since = parse_http_date_safe(request.META.get("HTTP_IF_MODIFIED_SINCE")) + if if_modified_since and if_modified_since >= calendar.timegm( + rev.created.utctimetuple() + ): response = HttpResponseNotModified() - response['Last-Modified'] = convert_to_http_date(rev.created) + response["Last-Modified"] = convert_to_http_date(rev.created) return response if settings.ATTACHMENTS_USE_S3: response = redirect(rev.file.url) else: response = StreamingHttpResponse(rev.file, content_type=rev.mime_type) - response['Content-Length'] = rev.file.size + response["Content-Length"] = rev.file.size - response['Last-Modified'] = convert_to_http_date(rev.created) - response['X-Frame-Options'] = f'ALLOW-FROM {settings.DOMAIN}' + response["Last-Modified"] = convert_to_http_date(rev.created) + response["X-Frame-Options"] = f"ALLOW-FROM {settings.DOMAIN}" return response @@ -89,12 +94,8 @@ def edit_attachment(request, document_slug, document_locale): Redirects back to the document's editing URL on success. """ - document = get_object_or_404( - Document, - locale=document_locale, - slug=document_slug, - ) - if request.method != 'POST': + document = get_object_or_404(Document, locale=document_locale, slug=document_slug,) + if request.method != "POST": return redirect(document.get_edit_url()) # No access if no permissions to upload @@ -106,7 +107,7 @@ def edit_attachment(request, document_slug, document_locale): files=request.FILES, # Only staff users are allowed to upload SVG files because SVG files # can contain embedded inline scripts. - allow_svg_uploads=request.user.is_staff + allow_svg_uploads=request.user.is_staff, ) if form.is_valid(): revision = form.save(commit=False) @@ -119,7 +120,7 @@ def edit_attachment(request, document_slug, document_locale): return redirect(document.get_edit_url()) else: context = { - 'form': form, - 'document': document, + "form": form, + "document": document, } - return render(request, 'attachments/edit_attachment.html', context) + return render(request, "attachments/edit_attachment.html", context) diff --git a/kuma/authkeys/admin.py b/kuma/authkeys/admin.py index 75ebe6f5f88..b5bb756ce3a 100644 --- a/kuma/authkeys/admin.py +++ b/kuma/authkeys/admin.py @@ -7,51 +7,46 @@ def history_link(self): - url = ( - f'{reverse("admin:authkeys_keyaction_changelist")}' - f'?key__exact={self.id}') + url = f'{reverse("admin:authkeys_keyaction_changelist")}' f"?key__exact={self.id}" count = self.history.count() - what = 'action' if count == 1 else 'actions' + what = "action" if count == 1 else "actions" return format_html('{} {}', url, count, what) -history_link.short_description = 'Usage history' +history_link.short_description = "Usage history" @admin.register(Key) class KeyAdmin(admin.ModelAdmin): - fields = ('description',) - list_display = ('id', 'user', 'created', history_link, 'key', - 'description') - ordering = ('-created', 'user') - search_fields = ('key', 'description', 'user__username') + fields = ("description",) + list_display = ("id", "user", "created", history_link, "key", "description") + ordering = ("-created", "user") + search_fields = ("key", "description", "user__username") def key_link(self): key = self.key - url = reverse('admin:authkeys_key_change', - args=[key.id]) + url = reverse("admin:authkeys_key_change", args=[key.id]) return format_html('{} (#{})', url, key.user, key.id) -key_link.short_description = 'Key' +key_link.short_description = "Key" def content_object_link(self): obj = self.content_object - url_key = f'admin:{obj._meta.app_label}_{obj._meta.model_name}_change' + url_key = f"admin:{obj._meta.app_label}_{obj._meta.model_name}_change" url = reverse(url_key, args=[obj.id]) return format_html('{} (#{})', url, self.content_type, obj.pk) -content_object_link.short_description = 'Object' +content_object_link.short_description = "Object" @admin.register(KeyAction) class KeyActionAdmin(admin.ModelAdmin): - fields = ('notes',) - list_display = ('id', 'created', key_link, 'action', - content_object_link, 'notes') - list_filter = ('action', 'content_type') - ordering = ('-id',) - search_fields = ('action', 'key__key', 'key__user__username', 'notes') + fields = ("notes",) + list_display = ("id", "created", key_link, "action", content_object_link, "notes") + list_filter = ("action", "content_type") + ordering = ("-id",) + search_fields = ("action", "key__key", "key__user__username", "notes") diff --git a/kuma/authkeys/decorators.py b/kuma/authkeys/decorators.py index 46c4546df48..e3c83da242b 100644 --- a/kuma/authkeys/decorators.py +++ b/kuma/authkeys/decorators.py @@ -14,17 +14,18 @@ def accepts_auth_key(func): On successful auth, the request will be set with the authkey and the user owning the key """ + @wraps(func) def process(request, *args, **kwargs): request.authkey = None if not settings.MAINTENANCE_MODE: - http_auth = request.META.get('HTTP_AUTHORIZATION', '') + http_auth = request.META.get("HTTP_AUTHORIZATION", "") if http_auth: try: - basic, b64_auth = http_auth.split(' ', 1) - if 'Basic' == basic: + basic, b64_auth = http_auth.split(" ", 1) + if "Basic" == basic: auth = base64.decodebytes(b64_auth.encode()).decode() - key_id, secret = auth.split(':', 1) + key_id, secret = auth.split(":", 1) key = Key.objects.get(key=key_id) if key.check_secret(secret): request.authkey = key diff --git a/kuma/authkeys/forms.py b/kuma/authkeys/forms.py index f9779b87437..fef0b8ad178 100644 --- a/kuma/authkeys/forms.py +++ b/kuma/authkeys/forms.py @@ -6,4 +6,4 @@ class KeyForm(forms.ModelForm): class Meta: model = Key - fields = ('description',) + fields = ("description",) diff --git a/kuma/authkeys/migrations/0001_initial.py b/kuma/authkeys/migrations/0001_initial.py index 6fd8038450e..6ed70b0c6fc 100644 --- a/kuma/authkeys/migrations/0001_initial.py +++ b/kuma/authkeys/migrations/0001_initial.py @@ -1,5 +1,3 @@ - - from django.db import models, migrations from django.conf import settings @@ -8,37 +6,86 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('contenttypes', '0001_initial'), + ("contenttypes", "0001_initial"), ] operations = [ migrations.CreateModel( - name='Key', + name="Key", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('key', models.CharField(verbose_name='Lookup key', max_length=64, editable=False, db_index=True)), - ('hashed_secret', models.CharField(verbose_name='Hashed secret', max_length=128, editable=False)), - ('description', models.TextField(verbose_name='Description of intended use')), - ('created', models.DateTimeField(auto_now_add=True)), - ('user', models.ForeignKey(editable=False, to=settings.AUTH_USER_MODEL, on_delete=models.PROTECT)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "key", + models.CharField( + verbose_name="Lookup key", + max_length=64, + editable=False, + db_index=True, + ), + ), + ( + "hashed_secret", + models.CharField( + verbose_name="Hashed secret", max_length=128, editable=False + ), + ), + ( + "description", + models.TextField(verbose_name="Description of intended use"), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ( + "user", + models.ForeignKey( + editable=False, + to=settings.AUTH_USER_MODEL, + on_delete=models.PROTECT, + ), + ), ], - options={ - }, + options={}, bases=(models.Model,), ), migrations.CreateModel( - name='KeyAction', + name="KeyAction", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('action', models.CharField(max_length=128)), - ('notes', models.TextField(null=True)), - ('object_id', models.PositiveIntegerField()), - ('created', models.DateTimeField(auto_now_add=True)), - ('content_type', models.ForeignKey(to='contenttypes.ContentType', on_delete=models.CASCADE)), - ('key', models.ForeignKey(related_name='history', to='authkeys.Key', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("action", models.CharField(max_length=128)), + ("notes", models.TextField(null=True)), + ("object_id", models.PositiveIntegerField()), + ("created", models.DateTimeField(auto_now_add=True)), + ( + "content_type", + models.ForeignKey( + to="contenttypes.ContentType", on_delete=models.CASCADE + ), + ), + ( + "key", + models.ForeignKey( + related_name="history", + to="authkeys.Key", + on_delete=models.CASCADE, + ), + ), ], - options={ - }, + options={}, bases=(models.Model,), ), ] diff --git a/kuma/authkeys/models.py b/kuma/authkeys/models.py index c9058ce574f..c41ad19f3fe 100644 --- a/kuma/authkeys/models.py +++ b/kuma/authkeys/models.py @@ -1,5 +1,3 @@ - - import base64 import hashlib import random @@ -17,9 +15,9 @@ def generate_key(): # 32 * 8 = 256 random bits random_bytes = bytes(random.randint(0, 255) for _ in range(32)) random_hash = hashlib.sha256(random_bytes).digest() - replacements = [b'rA', b'aZ', b'gQ', b'hH', b'hG', b'aR', b'DD'] + replacements = [b"rA", b"aZ", b"gQ", b"hH", b"hG", b"aR", b"DD"] random_repl = random.choice(replacements) - return base64.b64encode(random_hash, random_repl).rstrip(b'=').decode() + return base64.b64encode(random_hash, random_repl).rstrip(b"=").decode() def hash_secret(secret): @@ -28,19 +26,26 @@ def hash_secret(secret): class Key(models.Model): """Authentication key""" - user = models.ForeignKey(settings.AUTH_USER_MODEL, editable=False, - db_index=True, blank=False, null=False, - on_delete=models.PROTECT) - key = models.CharField(_("Lookup key"), max_length=64, - editable=False, db_index=True) - hashed_secret = models.CharField(_("Hashed secret"), max_length=128, - editable=False, db_index=False) - description = models.TextField(_("Description of intended use"), - blank=False) + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + editable=False, + db_index=True, + blank=False, + null=False, + on_delete=models.PROTECT, + ) + key = models.CharField( + _("Lookup key"), max_length=64, editable=False, db_index=True + ) + hashed_secret = models.CharField( + _("Hashed secret"), max_length=128, editable=False, db_index=False + ) + description = models.TextField(_("Description of intended use"), blank=False) created = models.DateTimeField(auto_now_add=True) def __str__(self): - return f'' + return f"" def generate_secret(self): self.key = generate_key() @@ -54,19 +59,22 @@ def check_secret(self, secret): return constant_time_compare(hash_secret(secret), self.hashed_secret) def log(self, action, content_object=None, notes=None): - action = KeyAction(key=self, action=action, - content_object=content_object, notes=notes) + action = KeyAction( + key=self, action=action, content_object=content_object, notes=notes + ) action.save() return action class KeyAction(models.Model): """Record of an action taken while using a key""" - key = models.ForeignKey(Key, related_name='history', db_index=True, - on_delete=models.CASCADE) + + key = models.ForeignKey( + Key, related_name="history", db_index=True, on_delete=models.CASCADE + ) action = models.CharField(max_length=128, blank=False) notes = models.TextField(null=True) content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() - content_object = GenericForeignKey('content_type', 'object_id') + content_object = GenericForeignKey("content_type", "object_id") created = models.DateTimeField(auto_now_add=True) diff --git a/kuma/authkeys/tests/conftest.py b/kuma/authkeys/tests/conftest.py index e75a0488559..d449fa31527 100644 --- a/kuma/authkeys/tests/conftest.py +++ b/kuma/authkeys/tests/conftest.py @@ -19,8 +19,4 @@ def user_auth_key(): secret = key.generate_secret() key.save() - return Object( - user=u, - key=key, - secret=secret, - ) + return Object(user=u, key=key, secret=secret,) diff --git a/kuma/authkeys/tests/test_decorators.py b/kuma/authkeys/tests/test_decorators.py index 200c2ae8fca..5262f22d0e0 100644 --- a/kuma/authkeys/tests/test_decorators.py +++ b/kuma/authkeys/tests/test_decorators.py @@ -17,26 +17,28 @@ def fake_view(request, foo, bar): @pytest.mark.parametrize("maintenance_mode", [False, True]) @pytest.mark.parametrize( "use_valid_key, use_valid_secret", - [(True, True), (True, False), (False, True), (False, False)] + [(True, True), (True, False), (False, True), (False, False)], ) -def test_auth_key_decorator(user_auth_key, settings, use_valid_key, - use_valid_secret, maintenance_mode): +def test_auth_key_decorator( + user_auth_key, settings, use_valid_key, use_valid_secret, maintenance_mode +): request = HttpRequest() request.user = AnonymousUser() auth = ( f'{user_auth_key.key.key if use_valid_key else "FAKE"}:' - f'{user_auth_key.secret if use_valid_secret else "FAKE"}') + f'{user_auth_key.secret if use_valid_secret else "FAKE"}' + ) b64_auth = base64.encodebytes(auth.encode()).decode() - request.META['HTTP_AUTHORIZATION'] = f'Basic {b64_auth}' + request.META["HTTP_AUTHORIZATION"] = f"Basic {b64_auth}" settings.MAINTENANCE_MODE = maintenance_mode - foo, bar = fake_view(request, 'foo', 'bar') + foo, bar = fake_view(request, "foo", "bar") - assert foo == 'foo' - assert bar == 'bar' + assert foo == "foo" + assert bar == "bar" if maintenance_mode or not (use_valid_key and use_valid_secret): assert not request.user.is_authenticated @@ -54,12 +56,12 @@ def test_auth_key_decorator_with_invalid_header(user_auth_key, settings): # Test with incorrect auth header request = HttpRequest() request.user = AnonymousUser() - request.META['HTTP_AUTHORIZATION'] = "Basic bad_auth_string" + request.META["HTTP_AUTHORIZATION"] = "Basic bad_auth_string" settings.MAINTENANCE_MODE = False # Make a request to the view - fake_view(request, 'foo', 'bar') + fake_view(request, "foo", "bar") # The user should not be authenticated and no error should be raised. assert not request.user.is_authenticated diff --git a/kuma/authkeys/tests/test_views.py b/kuma/authkeys/tests/test_views.py index 3c3e07e4667..8622248c56e 100644 --- a/kuma/authkeys/tests/test_views.py +++ b/kuma/authkeys/tests/test_views.py @@ -14,7 +14,6 @@ class RefetchingUserTestCase(TestCase): - def _cache_bust_user_perms(self): # method to cache-bust the user perms by re-fetching from DB # https://docs.djangoproject.com/en/1.7/topics/auth/default/#permissions-and-authorization @@ -22,61 +21,59 @@ def _cache_bust_user_perms(self): class KeyViewsTest(RefetchingUserTestCase): - def setUp(self): - username = 'tester23' - password = 'trustno1' - email = 'tester23@example.com' + username = "tester23" + password = "trustno1" + email = "tester23@example.com" - self.user = user(username=username, email=email, - password=password, save=True) + self.user = user(username=username, email=email, password=password, save=True) self.client.login(username=username, password=password) # Give self.user (tester23) keys permissions - add_perm = Permission.objects.get(codename='add_key') - del_perm = Permission.objects.get(codename='delete_key') + add_perm = Permission.objects.get(codename="add_key") + del_perm = Permission.objects.get(codename="delete_key") self.user.user_permissions.add(add_perm) self.user.user_permissions.add(del_perm) self._cache_bust_user_perms() - username2 = 'someone' - password2 = 'somepass' - email2 = 'someone@example.com' + username2 = "someone" + password2 = "somepass" + email2 = "someone@example.com" - self.user2 = user(username=username2, email=email2, - password=password2, save=True) + self.user2 = user( + username=username2, email=email2, password=password2, save=True + ) - self.key1 = Key(user=self.user, description='Test Key 1') + self.key1 = Key(user=self.user, description="Test Key 1") self.key1.save() - self.key2 = Key(user=self.user, description='Test Key 2') + self.key2 = Key(user=self.user, description="Test Key 2") self.key2.save() - self.key3 = Key(user=self.user2, description='Test Key 3') + self.key3 = Key(user=self.user2, description="Test Key 3") self.key3.save() def test_new_key(self): data = {"description": "This is meant for a test app"} - url = reverse('authkeys.new') + url = reverse("authkeys.new") # Check out the creation page, look for the form. resp = self.client.get(url, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 200 assert_no_cache_header(resp) page = pq(resp.content) - assert page.find('form.key').length == 1 + assert page.find("form.key").length == 1 # We don't have this key yet, right? - keys = Key.objects.filter(description=data['description']) + keys = Key.objects.filter(description=data["description"]) assert keys.count() == 0 # Okay, create it. - resp = self.client.post(url, data, follow=False, - HTTP_HOST=settings.WIKI_HOST) + resp = self.client.post(url, data, follow=False, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 200 assert_no_cache_header(resp) # We have the key now, right? - keys = Key.objects.filter(description=data['description']) + keys = Key.objects.filter(description=data["description"]) assert keys.count() == 1 # Okay, and it should belong to the logged-in user @@ -85,32 +82,32 @@ def test_new_key(self): # Take a look at the description and key shown on the result page. page = pq(resp.content) - assert page.find('.key .description').text() == data['description'] - assert page.find('.key .key').text() == key.key + assert page.find(".key .description").text() == data["description"] + assert page.find(".key .key").text() == key.key # Ensure the secret on the page checks out. - secret = page.find('.key .secret').text() + secret = page.find(".key .secret").text() assert key.check_secret(secret) def test_list_key(self): """The current user's keys should be shown, but only that user's""" - url = reverse('authkeys.list') + url = reverse("authkeys.list") resp = self.client.get(url, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 200 assert_no_cache_header(resp) page = pq(resp.content) for ct, key in ((1, self.key1), (1, self.key2), (0, self.key3)): - key_row = page.find('.option-list #key-%s' % key.pk) + key_row = page.find(".option-list #key-%s" % key.pk) assert key_row.length == ct if ct > 0: - assert key_row.find('.description').text() == key.description + assert key_row.find(".description").text() == key.description def test_key_history(self): # Assemble some sample log lines log_lines = [] for i in range(0, ITEMS_PER_PAGE * 2): - log_lines.append(('ping', self.user2, 'Number #%s' % i)) + log_lines.append(("ping", self.user2, "Number #%s" % i)) # Record the log lines for this key for l in log_lines: @@ -120,27 +117,29 @@ def test_key_history(self): log_lines.reverse() # Iterate through 2 expected pages... - for qs, offset in (('', 0), ('?page=2', ITEMS_PER_PAGE)): - url = reverse('authkeys.history', args=(self.key1.pk,)) + qs + for qs, offset in (("", 0), ("?page=2", ITEMS_PER_PAGE)): + url = reverse("authkeys.history", args=(self.key1.pk,)) + qs resp = self.client.get(url, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 200 assert_no_cache_header(resp) page = pq(resp.content) - rows = page.find('.item') + rows = page.find(".item") for idx in range(0, ITEMS_PER_PAGE): row = rows.eq(idx) expected = log_lines[idx + offset] - line = (row.find('.action').text(), - row.find('.object').text(), - row.find('.notes').text()) + line = ( + row.find(".action").text(), + row.find(".object").text(), + row.find(".notes").text(), + ) assert line[0] == expected[0] - assert ('%s' % expected[1]) in line[1] + assert ("%s" % expected[1]) in line[1] assert line[2] == expected[2] def test_delete_key(self): """User should be able to delete own keys, but no one else's""" - url = reverse('authkeys.delete', args=(self.key3.pk,)) + url = reverse("authkeys.delete", args=(self.key3.pk,)) resp = self.client.get(url, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 403 assert_no_cache_header(resp) @@ -149,13 +148,13 @@ def test_delete_key(self): assert resp.status_code == 403 assert_no_cache_header(resp) - url = reverse('authkeys.delete', args=(self.key1.pk,)) + url = reverse("authkeys.delete", args=(self.key1.pk,)) resp = self.client.get(url, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 200 assert_no_cache_header(resp) page = pq(resp.content) - assert page.find('.description').text() == self.key1.description + assert page.find(".description").text() == self.key1.description resp = self.client.post(url, follow=False, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 302 @@ -165,23 +164,21 @@ def test_delete_key(self): class KeyViewsPermissionTest(RefetchingUserTestCase): - def setUp(self): - username = 'tester23' - password = 'trustno1' - email = 'tester23@example.com' + username = "tester23" + password = "trustno1" + email = "tester23@example.com" - self.user = user(username=username, email=email, - password=password, save=True) + self.user = user(username=username, email=email, password=password, save=True) self.client.login(username=username, password=password) def test_new_key_requires_permission(self): - url = reverse('authkeys.new') + url = reverse("authkeys.new") resp = self.client.get(url, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 403 assert_no_cache_header(resp) - perm = Permission.objects.get(codename='add_key') + perm = Permission.objects.get(codename="add_key") self.user.user_permissions.add(perm) self._cache_bust_user_perms() @@ -190,10 +187,10 @@ def test_new_key_requires_permission(self): assert_no_cache_header(resp) def test_delete_key_requires_separate_permission(self): - self.key1 = Key(user=self.user, description='Test Key 1') + self.key1 = Key(user=self.user, description="Test Key 1") self.key1.save() - url = reverse('authkeys.delete', args=(self.key1.pk,)) + url = reverse("authkeys.delete", args=(self.key1.pk,)) resp = self.client.get(url, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 403 assert_no_cache_header(resp) @@ -203,7 +200,7 @@ def test_delete_key_requires_separate_permission(self): assert resp.status_code == 403 assert_no_cache_header(resp) - perm = Permission.objects.get(codename='delete_key') + perm = Permission.objects.get(codename="delete_key") self.user.user_permissions.add(perm) self._cache_bust_user_perms() @@ -212,9 +209,11 @@ def test_delete_key_requires_separate_permission(self): assert_no_cache_header(resp) -@pytest.mark.parametrize('endpoint', ['new', 'list', 'history', 'delete']) +@pytest.mark.parametrize("endpoint", ["new", "list", "history", "delete"]) def test_redirect(client, endpoint): - url = reverse('authkeys.{}'.format(endpoint), - args=(1,) if endpoint in ('history', 'delete') else ()) + url = reverse( + "authkeys.{}".format(endpoint), + args=(1,) if endpoint in ("history", "delete") else (), + ) response = client.get(url) assert_redirect_to_wiki(response, url) diff --git a/kuma/authkeys/urls.py b/kuma/authkeys/urls.py index f4c6a0c07fa..2fd82e561bd 100644 --- a/kuma/authkeys/urls.py +++ b/kuma/authkeys/urls.py @@ -3,13 +3,7 @@ from . import views urlpatterns = [ - url(r'^new$', - views.new, - name='authkeys.new'), - url(r'^(?P\d+)/history$', - views.history, - name='authkeys.history'), - url(r'^(?P\d+)/delete$', - views.delete, - name='authkeys.delete'), + url(r"^new$", views.new, name="authkeys.new"), + url(r"^(?P\d+)/history$", views.history, name="authkeys.history"), + url(r"^(?P\d+)/delete$", views.delete, name="authkeys.delete"), ] diff --git a/kuma/authkeys/views.py b/kuma/authkeys/views.py index 98b4d317d69..fae1d43997d 100644 --- a/kuma/authkeys/views.py +++ b/kuma/authkeys/views.py @@ -14,28 +14,28 @@ @ensure_wiki_domain @login_required -@permission_required('authkeys.add_key', raise_exception=True) +@permission_required("authkeys.add_key", raise_exception=True) def new(request): context = {"key": None} if request.method != "POST": - context['form'] = KeyForm() + context["form"] = KeyForm() else: - context['form'] = KeyForm(request.POST) - if context['form'].is_valid(): - new_key = context['form'].save(commit=False) + context["form"] = KeyForm(request.POST) + if context["form"].is_valid(): + new_key = context["form"].save(commit=False) new_key.user = request.user - context['secret'] = new_key.generate_secret() + context["secret"] = new_key.generate_secret() new_key.save() - context['key'] = new_key + context["key"] = new_key - return render(request, 'authkeys/new.html', context) + return render(request, "authkeys/new.html", context) @ensure_wiki_domain @login_required def list(request): keys = Key.objects.filter(user=request.user) - return render(request, 'authkeys/list.html', dict(keys=keys)) + return render(request, "authkeys/list.html", dict(keys=keys)) @ensure_wiki_domain @@ -44,23 +44,23 @@ def history(request, pk): key = get_object_or_404(Key, pk=pk) if key.user != request.user: raise PermissionDenied - items = key.history.all().order_by('-pk') + items = key.history.all().order_by("-pk") items = paginate(request, items, per_page=ITEMS_PER_PAGE) context = { - 'key': key, - 'items': items, + "key": key, + "items": items, } - return render(request, 'authkeys/history.html', context) + return render(request, "authkeys/history.html", context) @ensure_wiki_domain @login_required -@permission_required('authkeys.delete_key', raise_exception=True) +@permission_required("authkeys.delete_key", raise_exception=True) def delete(request, pk): key = get_object_or_404(Key, pk=pk) if key.user != request.user: raise PermissionDenied if request.method == "POST": key.delete() - return redirect('authkeys.list') - return render(request, 'authkeys/delete.html', {'key': key}) + return redirect("authkeys.list") + return render(request, "authkeys/delete.html", {"key": key}) diff --git a/kuma/banners/admin.py b/kuma/banners/admin.py index 5b40e3c939d..3a47abaf77b 100644 --- a/kuma/banners/admin.py +++ b/kuma/banners/admin.py @@ -1,5 +1,3 @@ - - from django.contrib import admin from .models import Banner @@ -7,28 +5,35 @@ @admin.register(Banner) class BannerAdmin(admin.ModelAdmin): - actions = ['activate_all', 'deactivate_all'] - list_display = ('name', 'active', 'priority') - fields = ('name', 'title', 'main_copy', 'button_copy', - 'theme', 'active', 'priority') - search_fields = ('name', 'active') - ordering = ('priority',) + actions = ["activate_all", "deactivate_all"] + list_display = ("name", "active", "priority") + fields = ( + "name", + "title", + "main_copy", + "button_copy", + "theme", + "active", + "priority", + ) + search_fields = ("name", "active") + ordering = ("priority",) def activate_all(self, request, queryset): rows_updated = queryset.update(active=True) if rows_updated == 1: - message_bit = '1 banner was' + message_bit = "1 banner was" else: - message_bit = '%s banners were ' % rows_updated - self.message_user(request, '%s successfully marked as active.' % message_bit) + message_bit = "%s banners were " % rows_updated + self.message_user(request, "%s successfully marked as active." % message_bit) def deactivate_all(self, request, queryset): rows_updated = queryset.update(active=False) if rows_updated == 1: - message_bit = '1 banner was' + message_bit = "1 banner was" else: - message_bit = '%s banners were ' % rows_updated - self.message_user(request, '%s successfully deactived.' % message_bit) + message_bit = "%s banners were " % rows_updated + self.message_user(request, "%s successfully deactived." % message_bit) - activate_all.short_description = 'Activate all selected banners' - deactivate_all.short_description = 'Deactivate all selected banners' + activate_all.short_description = "Activate all selected banners" + deactivate_all.short_description = "Deactivate all selected banners" diff --git a/kuma/banners/migrations/0001_initial.py b/kuma/banners/migrations/0001_initial.py index 6fd64502b95..9e6a91462ef 100644 --- a/kuma/banners/migrations/0001_initial.py +++ b/kuma/banners/migrations/0001_initial.py @@ -8,21 +8,54 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Banner', + name="Banner", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=50, verbose_name='Banner Name')), - ('title', models.CharField(max_length=100, verbose_name='Banner Title')), - ('main_copy', models.TextField(max_length=200, verbose_name='Main Copy')), - ('button_copy', models.CharField(max_length=50, verbose_name='Button Copy')), - ('theme', models.CharField(choices=[(b'default', b'Default'), (b'gradient', b'Gradient'), (b'dinohead', b'Dinohead')], default=b'default', max_length=20, verbose_name='Theme')), - ('active', models.BooleanField(verbose_name='Activate')), - ('priority', models.PositiveSmallIntegerField(default=100, verbose_name='Priority (1-100)')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=50, verbose_name="Banner Name")), + ( + "title", + models.CharField(max_length=100, verbose_name="Banner Title"), + ), + ( + "main_copy", + models.TextField(max_length=200, verbose_name="Main Copy"), + ), + ( + "button_copy", + models.CharField(max_length=50, verbose_name="Button Copy"), + ), + ( + "theme", + models.CharField( + choices=[ + (b"default", b"Default"), + (b"gradient", b"Gradient"), + (b"dinohead", b"Dinohead"), + ], + default=b"default", + max_length=20, + verbose_name="Theme", + ), + ), + ("active", models.BooleanField(verbose_name="Activate")), + ( + "priority", + models.PositiveSmallIntegerField( + default=100, verbose_name="Priority (1-100)" + ), + ), ], ), ] diff --git a/kuma/banners/migrations/0002_auto_20191023_0405.py b/kuma/banners/migrations/0002_auto_20191023_0405.py index f5db0ed2536..0798c8527ee 100644 --- a/kuma/banners/migrations/0002_auto_20191023_0405.py +++ b/kuma/banners/migrations/0002_auto_20191023_0405.py @@ -7,13 +7,22 @@ class Migration(migrations.Migration): dependencies = [ - ('banners', '0001_initial'), + ("banners", "0001_initial"), ] operations = [ migrations.AlterField( - model_name='banner', - name='theme', - field=models.CharField(choices=[('default', 'Default'), ('gradient', 'Gradient'), ('dinohead', 'Dinohead')], default='default', max_length=20, verbose_name='Theme'), + model_name="banner", + name="theme", + field=models.CharField( + choices=[ + ("default", "Default"), + ("gradient", "Gradient"), + ("dinohead", "Dinohead"), + ], + default="default", + max_length=20, + verbose_name="Theme", + ), ), ] diff --git a/kuma/banners/models.py b/kuma/banners/models.py index f041b189a5e..cd72a83f71e 100644 --- a/kuma/banners/models.py +++ b/kuma/banners/models.py @@ -1,5 +1,3 @@ - - from django.db import models @@ -7,23 +5,24 @@ class Banner(models.Model): """ Defines the model for a single call to action banner """ - THEME_DEFAULT = 'default' - THEME_GRADIENT = 'gradient' - THEME_DINOHEAD = 'dinohead' + + THEME_DEFAULT = "default" + THEME_GRADIENT = "gradient" + THEME_DINOHEAD = "dinohead" THEMES = ( - (THEME_DEFAULT, 'Default'), - (THEME_GRADIENT, 'Gradient'), - (THEME_DINOHEAD, 'Dinohead') + (THEME_DEFAULT, "Default"), + (THEME_GRADIENT, "Gradient"), + (THEME_DINOHEAD, "Dinohead"), + ) + name = models.CharField("Banner Name", max_length=50) + title = models.CharField("Banner Title", max_length=100) + main_copy = models.TextField("Main Copy", max_length=200) + button_copy = models.CharField("Button Copy", max_length=50) + theme = models.CharField( + "Theme", max_length=20, choices=THEMES, default=THEME_DEFAULT ) - name = models.CharField('Banner Name', max_length=50) - title = models.CharField('Banner Title', max_length=100) - main_copy = models.TextField('Main Copy', max_length=200) - button_copy = models.CharField('Button Copy', max_length=50) - theme = models.CharField('Theme', max_length=20, - choices=THEMES, default=THEME_DEFAULT) - active = models.BooleanField('Activate') - priority = models.PositiveSmallIntegerField('Priority (1-100)', - default=100) + active = models.BooleanField("Activate") + priority = models.PositiveSmallIntegerField("Priority (1-100)", default=100) def __str__(self): return self.name diff --git a/kuma/banners/tests/test_models.py b/kuma/banners/tests/test_models.py index a316d304426..af60b3478ca 100644 --- a/kuma/banners/tests/test_models.py +++ b/kuma/banners/tests/test_models.py @@ -1,5 +1,3 @@ - - import pytest from ..models import Banner @@ -15,7 +13,7 @@ def test_add_new_banner(): "button_copy": "Click Me!", "theme": "default", "active": True, - "priority": "2" + "priority": "2", } banner = Banner.objects.create(**sample_banner) @@ -31,7 +29,7 @@ def test_default_theme_set(): "main_copy": "Some sample main copy", "button_copy": "Click Me!", "active": False, - "priority": "1" + "priority": "1", } banner = Banner.objects.create(**sample_banner) @@ -47,7 +45,7 @@ def test_default_priority_set(): "main_copy": "Some sample main copy", "button_copy": "Click Me!", "theme": "default", - "active": False + "active": False, } banner = Banner.objects.create(**sample_banner) @@ -64,7 +62,7 @@ def test_activate_banner(): "button_copy": "Click Me!", "theme": "default", "active": False, - "priority": "1" + "priority": "1", } banner = Banner.objects.create(**sample_banner) assert banner.active is False diff --git a/kuma/celery.py b/kuma/celery.py index 1fc2656a8fb..16fa7221855 100644 --- a/kuma/celery.py +++ b/kuma/celery.py @@ -1,19 +1,17 @@ - - import os from celery import Celery # set the default Django settings module for the 'celery' program. -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'kuma.settings.local') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "kuma.settings.local") -app = Celery('kuma') +app = Celery("kuma") # Using a string here means the worker doesn't have to serialize # the configuration object to child processes. # - namespace='CELERY' means all celery-related configuration keys # should have a `CELERY_` prefix. -app.config_from_object('django.conf:settings', namespace='CELERY') +app.config_from_object("django.conf:settings", namespace="CELERY") # Load task modules from all registered Django app configs. app.autodiscover_tasks() @@ -21,7 +19,7 @@ @app.task(bind=True) def debug_task(self): - print('Request: {0!r}'.format(self.request)) + print("Request: {0!r}".format(self.request)) @app.task() @@ -30,6 +28,7 @@ def debug_task_returning(a, b): And it also checks that called with a `datetime.date` it gets that as parameters in the task.""" import datetime + assert isinstance(a, datetime.date), type(a) assert isinstance(b, datetime.date), type(b) return a < b diff --git a/kuma/conftest.py b/kuma/conftest.py index 27d60051ac2..ce1b8c547ed 100644 --- a/kuma/conftest.py +++ b/kuma/conftest.py @@ -24,7 +24,7 @@ def clear_cache(): @pytest.fixture(autouse=True) def set_default_language(): - activate('en-US') + activate("en-US") @pytest.fixture(autouse=True) @@ -60,10 +60,12 @@ def reset_urlconf(): class ConstanceConfigWrapper(object): """A Constance configuration wrapper to allow overriding the config.""" + _original_values = [] def __setattr__(self, attr, value): from constance import config + self._original_values.append((attr, getattr(config, attr))) setattr(config, attr, value) # This can fail if Constance uses a cached database backend @@ -72,6 +74,7 @@ def __setattr__(self, attr, value): def finalize(self): from constance import config + for attr, value in reversed(self._original_values): setattr(config, attr, value) del self._original_values[:] @@ -87,28 +90,29 @@ def constance_config(db, settings): @pytest.fixture def beta_testers_group(db): - return Group.objects.create(name='Beta Testers') + return Group.objects.create(name="Beta Testers") @pytest.fixture def wiki_user(db, django_user_model): """A test user.""" return django_user_model.objects.create( - username='wiki_user', - email='wiki_user@example.com', - date_joined=datetime(2017, 4, 14, 12, 0)) + username="wiki_user", + email="wiki_user@example.com", + date_joined=datetime(2017, 4, 14, 12, 0), + ) @pytest.fixture def wiki_user_github_account(wiki_user): return SocialAccount.objects.create( user=wiki_user, - provider='github', + provider="github", extra_data=dict( email=wiki_user.email, - avatar_url='https://avatars0.githubusercontent.com/yada/yada', - html_url="https://github.com/{}".format(wiki_user.username) - ) + avatar_url="https://avatars0.githubusercontent.com/yada/yada", + html_url="https://github.com/{}".format(wiki_user.username), + ), ) @@ -116,33 +120,35 @@ def wiki_user_github_account(wiki_user): def wiki_user_2(db, django_user_model): """A second test user.""" return django_user_model.objects.create( - username='wiki_user_2', - email='wiki_user_2@example.com', - date_joined=datetime(2017, 4, 17, 10, 30)) + username="wiki_user_2", + email="wiki_user_2@example.com", + date_joined=datetime(2017, 4, 17, 10, 30), + ) @pytest.fixture def wiki_user_3(db, django_user_model): """A third test user.""" return django_user_model.objects.create( - username='wiki_user_3', - email='wiki_user_3@example.com', - date_joined=datetime(2017, 4, 23, 11, 45)) + username="wiki_user_3", + email="wiki_user_3@example.com", + date_joined=datetime(2017, 4, 23, 11, 45), + ) @pytest.fixture def user_client(client, wiki_user): """A test client with wiki_user logged in.""" - wiki_user.set_password('password') + wiki_user.set_password("password") wiki_user.save() - client.login(username=wiki_user.username, password='password') + client.login(username=wiki_user.username, password="password") return client @pytest.fixture def editor_client(user_client): """A test client with wiki_user logged in for editing.""" - with override_flag('kumaediting', True): + with override_flag("kumaediting", True): yield user_client @@ -150,13 +156,15 @@ def editor_client(user_client): def root_doc(wiki_user): """A newly-created top-level English document.""" root_doc = Document.objects.create( - locale='en-US', slug='Root', title='Root Document') + locale="en-US", slug="Root", title="Root Document" + ) Revision.objects.create( document=root_doc, creator=wiki_user, - content='

Getting started...

', - title='Root Document', - created=datetime(2017, 4, 14, 12, 15)) + content="

Getting started...

", + title="Root Document", + created=datetime(2017, 4, 14, 12, 15), + ) return root_doc @@ -170,17 +178,19 @@ def create_revision(root_doc): def trans_doc(create_revision, wiki_user): """Translate the root document into French.""" trans_doc = Document.objects.create( - locale='fr', + locale="fr", parent=create_revision.document, - slug='Racine', - title='Racine du Document') + slug="Racine", + title="Racine du Document", + ) Revision.objects.create( document=trans_doc, creator=wiki_user, based_on=create_revision, - content='

Mise en route...

', - title='Racine du Document', - created=datetime(2017, 4, 14, 12, 20)) + content="

Mise en route...

", + title="Racine du Document", + created=datetime(2017, 4, 14, 12, 20), + ) return trans_doc @@ -188,16 +198,19 @@ def trans_doc(create_revision, wiki_user): def redirect_doc(wiki_user, root_doc): """A newly-created top-level English redirect document.""" redirect_doc = Document.objects.create( - locale='en-US', slug='Redirection', title='Redirect Document') + locale="en-US", slug="Redirection", title="Redirect Document" + ) Revision.objects.create( document=redirect_doc, creator=wiki_user, - content=REDIRECT_CONTENT % { - 'href': reverse('wiki.document', args=(root_doc.slug,)), - 'title': root_doc.title, + content=REDIRECT_CONTENT + % { + "href": reverse("wiki.document", args=(root_doc.slug,)), + "title": root_doc.title, }, - title='Redirect Document', - created=datetime(2017, 4, 17, 12, 15)) + title="Redirect Document", + created=datetime(2017, 4, 17, 12, 15), + ) return redirect_doc diff --git a/kuma/core/admin.py b/kuma/core/admin.py index b1b89b15a3a..32b9e644cd1 100644 --- a/kuma/core/admin.py +++ b/kuma/core/admin.py @@ -5,7 +5,6 @@ class DisabledDeleteActionMixin(object): - def get_actions(self, request): """ Remove the built-in delete action, since it bypasses the model @@ -13,13 +12,12 @@ def get_actions(self, request): deletion UI anyway. """ actions = super(DisabledDeleteActionMixin, self).get_actions(request) - if 'delete_selected' in actions: - del actions['delete_selected'] + if "delete_selected" in actions: + del actions["delete_selected"] return actions class DisabledDeletionMixin(DisabledDeleteActionMixin): - def has_delete_permission(self, request, obj=None): """ Disable deletion of individual Documents, by always returning @@ -32,8 +30,8 @@ def has_delete_permission(self, request, obj=None): class IPBanAdmin(admin.ModelAdmin): # Remove list delete action to enforce model soft delete in admin site actions = None - readonly_fields = ('deleted',) - list_display = ('ip', 'created', 'deleted') + readonly_fields = ("deleted",) + list_display = ("ip", "created", "deleted") -TokenAdmin.raw_id_fields = ['user'] +TokenAdmin.raw_id_fields = ["user"] diff --git a/kuma/core/apps.py b/kuma/core/apps.py index 125295fb220..3af42a2cfdd 100644 --- a/kuma/core/apps.py +++ b/kuma/core/apps.py @@ -1,5 +1,3 @@ - - from django.apps import AppConfig from django.conf import settings from django.utils.functional import cached_property @@ -13,18 +11,17 @@ class CoreConfig(AppConfig): The Django App Config class to store information about the core app and do startup time things. """ - name = 'kuma.core' - verbose_name = _('Core') + + name = "kuma.core" + verbose_name = _("Core") def ready(self): """Configure kuma.core after models are loaded.""" # Clean up expired sessions every 60 minutes from kuma.core.tasks import clean_sessions - app.add_periodic_task( - 60 * 60, - clean_sessions.s() - ) + + app.add_periodic_task(60 * 60, clean_sessions.s()) @cached_property def language_mapping(self): diff --git a/kuma/core/backends.py b/kuma/core/backends.py index c72922ceede..8ea6f03d706 100644 --- a/kuma/core/backends.py +++ b/kuma/core/backends.py @@ -1,5 +1,3 @@ - - from constance.backends.database import DatabaseBackend diff --git a/kuma/core/context_processors.py b/kuma/core/context_processors.py index f3c8335c121..54be6c22c72 100644 --- a/kuma/core/context_processors.py +++ b/kuma/core/context_processors.py @@ -11,13 +11,13 @@ def global_settings(request): """Adds settings to the context.""" def clean_safe_url(url): - if '://' not in url: + if "://" not in url: # E.g. 'elasticsearch:9200' - url = 'http://' + url + url = "http://" + url parsed = urlparse(url) - if '@' in parsed.netloc: + if "@" in parsed.netloc: parsed = parsed._replace( - netloc='username:secret@' + parsed.netloc.split('@')[-1] + netloc="username:secret@" + parsed.netloc.split("@")[-1] ) return parsed.geturl() @@ -28,36 +28,36 @@ def clean_safe_url(url): # and a valid value in the environment (for production!) then we # can delete these lines of code. # See https://bugzilla.mozilla.org/show_bug.cgi?id=1570076 - google_analytics_account = getattr( - settings, 'GOOGLE_ANALYTICS_ACCOUNT', None) + google_analytics_account = getattr(settings, "GOOGLE_ANALYTICS_ACCOUNT", None) if google_analytics_account is None: - if config.GOOGLE_ANALYTICS_ACCOUNT != '0': + if config.GOOGLE_ANALYTICS_ACCOUNT != "0": settings.GOOGLE_ANALYTICS_ACCOUNT = config.GOOGLE_ANALYTICS_ACCOUNT return { - 'settings': settings, - + "settings": settings, # Because the 'settings.ES_URLS' might contain the username:password # it's never appropriate to display in templates. So clean them up. # But return it as a lambda so it only executes if really needed. - 'safe_es_urls': lambda: [ - clean_safe_url(x) for x in settings.ES_URLS - ] + "safe_es_urls": lambda: [clean_safe_url(x) for x in settings.ES_URLS], } def i18n(request): return { - 'LANGUAGES': get_language_mapping(), - 'LANG': (settings.LANGUAGE_URL_MAP.get(translation.get_language()) or - translation.get_language()), - 'DIR': 'rtl' if translation.get_language_bidi() else 'ltr', + "LANGUAGES": get_language_mapping(), + "LANG": ( + settings.LANGUAGE_URL_MAP.get(translation.get_language()) + or translation.get_language() + ), + "DIR": "rtl" if translation.get_language_bidi() else "ltr", } def next_url(request): - if (hasattr(request, 'path') and - 'login' not in request.path and - 'register' not in request.path): - return {'next_url': request.get_full_path()} + if ( + hasattr(request, "path") + and "login" not in request.path + and "register" not in request.path + ): + return {"next_url": request.get_full_path()} return {} diff --git a/kuma/core/decorators.py b/kuma/core/decorators.py index 686c297d403..f4449a65e46 100644 --- a/kuma/core/decorators.py +++ b/kuma/core/decorators.py @@ -1,5 +1,3 @@ - - import re from functools import partial, wraps from urllib.parse import quote @@ -31,12 +29,14 @@ def shared_cache_control(func=None, **kwargs): cache for the default perioid of time - public - Allow intermediate proxies to cache response """ + def _shared_cache_controller(viewfunc): @wraps(viewfunc, assigned=available_attrs(viewfunc)) def _cache_controlled(request, *args, **kw): response = viewfunc(request, *args, **kw) add_shared_cache_control(response, **kwargs) return response + return _cache_controlled if func: @@ -44,8 +44,9 @@ def _cache_controlled(request, *args, **kw): return _shared_cache_controller -def user_access_decorator(redirect_func, redirect_url_func, deny_func=None, - redirect_field=REDIRECT_FIELD_NAME): +def user_access_decorator( + redirect_func, redirect_url_func, deny_func=None, redirect_field=REDIRECT_FIELD_NAME +): """ Helper function that returns a decorator. @@ -57,17 +58,18 @@ def user_access_decorator(redirect_func, redirect_url_func, deny_func=None, Set this to None to exclude it from the URL. """ + def decorator(view_fn): def _wrapped_view(request, *args, **kwargs): if redirect_func(request.user): # We must call reverse at the view level, else the threadlocal # locale prefixing doesn't take effect. - redirect_url = redirect_url_func() or reverse('account_login') + redirect_url = redirect_url_func() or reverse("account_login") # Redirect back here afterwards? if redirect_field: path = quote(request.get_full_path()) - redirect_url = f'{redirect_url}?{redirect_field}={path}' + redirect_url = f"{redirect_url}?{redirect_field}={path}" return HttpResponseRedirect(redirect_url) @@ -75,6 +77,7 @@ def _wrapped_view(request, *args, **kwargs): return HttpResponseForbidden() return view_fn(request, *args, **kwargs) + return wraps(view_fn, assigned=available_attrs(view_fn))(_wrapped_view) return decorator @@ -82,42 +85,51 @@ def _wrapped_view(request, *args, **kwargs): def logout_required(redirect): """Requires that the user *not* be logged in.""" - redirect_func = lambda u: u.is_authenticated - if hasattr(redirect, '__call__'): + redirect_func = lambda u: u.is_authenticated # noqa: E731 + if hasattr(redirect, "__call__"): return user_access_decorator( - redirect_func, redirect_field=None, - redirect_url_func=lambda: reverse('home'))(redirect) + redirect_func, + redirect_field=None, + redirect_url_func=lambda: reverse("home"), + )(redirect) else: - return user_access_decorator(redirect_func, redirect_field=None, - redirect_url_func=lambda: redirect) + return user_access_decorator( + redirect_func, redirect_field=None, redirect_url_func=lambda: redirect + ) -def login_required(func, login_url=None, redirect=REDIRECT_FIELD_NAME, - only_active=True): +def login_required( + func, login_url=None, redirect=REDIRECT_FIELD_NAME, only_active=True +): """Requires that the user is logged in.""" if only_active: - redirect_func = lambda u: not (u.is_authenticated and u.is_active) + redirect_func = lambda u: not (u.is_authenticated and u.is_active) # noqa: E731 else: - redirect_func = lambda u: not u.is_authenticated - redirect_url_func = lambda: login_url - return user_access_decorator(redirect_func, redirect_field=redirect, - redirect_url_func=redirect_url_func)(func) + redirect_func = lambda u: not u.is_authenticated # noqa: E731 + redirect_url_func = lambda: login_url # noqa: E731 + return user_access_decorator( + redirect_func, redirect_field=redirect, redirect_url_func=redirect_url_func + )(func) -def permission_required(perm, login_url=None, redirect=REDIRECT_FIELD_NAME, - only_active=True): +def permission_required( + perm, login_url=None, redirect=REDIRECT_FIELD_NAME, only_active=True +): """A replacement for django.contrib.auth.decorators.permission_required that doesn't ask authenticated users to log in.""" - redirect_func = lambda u: not u.is_authenticated + redirect_func = lambda u: not u.is_authenticated # noqa: E731 if only_active: - deny_func = lambda u: not (u.is_active and u.has_perm(perm)) + deny_func = lambda u: not (u.is_active and u.has_perm(perm)) # noqa: E731 else: - deny_func = lambda u: not u.has_perm(perm) - redirect_url_func = lambda: login_url + deny_func = lambda u: not u.has_perm(perm) # noqa: E731 + redirect_url_func = lambda: login_url # noqa: E731 - return user_access_decorator(redirect_func, redirect_field=redirect, - redirect_url_func=redirect_url_func, - deny_func=deny_func) + return user_access_decorator( + redirect_func, + redirect_field=redirect, + redirect_url_func=redirect_url_func, + deny_func=deny_func, + ) def is_superuser(u): @@ -133,21 +145,20 @@ def is_superuser(u): def block_user_agents(view_func): - blockable_user_agents = getattr(settings, 'BLOCKABLE_USER_AGENTS', []) + blockable_user_agents = getattr(settings, "BLOCKABLE_USER_AGENTS", []) blockable_ua_patterns = [] for agent in blockable_user_agents: blockable_ua_patterns.append(re.compile(agent)) def agent_blocked_view(request, *args, **kwargs): - http_user_agent = request.META.get('HTTP_USER_AGENT', None) + http_user_agent = request.META.get("HTTP_USER_AGENT", None) if http_user_agent is not None: for pattern in blockable_ua_patterns: - if pattern.search(request.META['HTTP_USER_AGENT']): + if pattern.search(request.META["HTTP_USER_AGENT"]): return HttpResponseForbidden() return view_func(request, *args, **kwargs) - return wraps(view_func, - assigned=available_attrs(view_func))(agent_blocked_view) + return wraps(view_func, assigned=available_attrs(view_func))(agent_blocked_view) def block_banned_ips(view_func): @@ -155,11 +166,10 @@ def block_banned_ips(view_func): @wraps(view_func) def block_if_banned(request, *args, **kwargs): - ip = request.META.get('REMOTE_ADDR', '10.0.0.1') + ip = request.META.get("REMOTE_ADDR", "10.0.0.1") banned_ips = BannedIPsJob().get() if ip in banned_ips: - return render(request, '403.html', - {'reason': 'ip_banned'}, status=403) + return render(request, "403.html", {"reason": "ip_banned"}, status=403) return view_func(request, *args, **kwargs) return block_if_banned @@ -171,6 +181,7 @@ def skip_in_maintenance_mode(func): the call to the decorated function. Otherwise, call the decorated function as usual. """ + @wraps(func) def wrapped(*args, **kwargs): if settings.MAINTENANCE_MODE: @@ -192,9 +203,10 @@ def redirect_in_maintenance_mode(func=None, methods=None): @wraps(func) def wrapped(request, *args, **kwargs): - if (settings.MAINTENANCE_MODE and - ((methods is None) or (request.method in methods))): - return redirect('maintenance_mode') + if settings.MAINTENANCE_MODE and ( + (methods is None) or (request.method in methods) + ): + return redirect("maintenance_mode") return func(request, *args, **kwargs) return wrapped @@ -205,6 +217,7 @@ def ensure_wiki_domain(func): Decorator for view functions. If this request is not for the Wiki domain, redirect it to that domain. """ + @wraps(func) def wrapped(request, *args, **kwargs): if not is_wiki(request): diff --git a/kuma/core/email_utils.py b/kuma/core/email_utils.py index 5a68cc510ce..c37a5c3a610 100644 --- a/kuma/core/email_utils.py +++ b/kuma/core/email_utils.py @@ -1,5 +1,3 @@ - - import logging from functools import wraps @@ -10,7 +8,7 @@ from django.utils import translation -log = logging.getLogger('kuma.core.email') +log = logging.getLogger("kuma.core.email") def safe_translation(f): @@ -20,6 +18,7 @@ def safe_translation(f): NB: This means `f` will be called up to two times! """ + @wraps(f) def wrapper(locale, *args, **kwargs): try: @@ -57,7 +56,7 @@ def _render(locale): Because of safe_translation decorator, if this fails, the function will be run again in English. """ - req = RequestFactory().get('/') + req = RequestFactory().get("/") req.META = {} req.LANGUAGE_CODE = locale @@ -66,14 +65,16 @@ def _render(locale): return _render(translation.get_language()) -def emails_with_users_and_watches(subject, - text_template, - html_template, - context_vars, - users_and_watches, - from_email=settings.TIDINGS_FROM_ADDRESS, - default_locale=settings.WIKI_DEFAULT_LANGUAGE, - **extra_kwargs): +def emails_with_users_and_watches( + subject, + text_template, + html_template, + context_vars, + users_and_watches, + from_email=settings.TIDINGS_FROM_ADDRESS, + default_locale=settings.WIKI_DEFAULT_LANGUAGE, + **extra_kwargs +): """Return iterable of EmailMessages with user and watch values substituted. A convenience function for generating emails by repeatedly @@ -97,27 +98,30 @@ def emails_with_users_and_watches(subject, :returns: generator of EmailMessage objects """ + @safe_translation def _make_mail(locale, user, watch): - context_vars['user'] = user - context_vars['watch'] = watch[0] - context_vars['watches'] = watch + context_vars["user"] = user + context_vars["watch"] = watch[0] + context_vars["watches"] = watch msg = EmailMultiAlternatives( subject % context_vars, render_email(text_template, context_vars), from_email, [user.email], - **extra_kwargs) + **extra_kwargs + ) if html_template: msg.attach_alternative( - render_email(html_template, context_vars), 'text/html') + render_email(html_template, context_vars), "text/html" + ) return msg for user, watch in users_and_watches: - if hasattr(user, 'locale'): + if hasattr(user, "locale"): locale = user.locale else: locale = default_locale diff --git a/kuma/core/exceptions.py b/kuma/core/exceptions.py index 272af5335d1..d4af007ec53 100644 --- a/kuma/core/exceptions.py +++ b/kuma/core/exceptions.py @@ -4,6 +4,7 @@ class ProgrammingError(Exception): class DateTimeFormatError(Exception): """Called by the datetimeformat function when receiving invalid format.""" + pass diff --git a/kuma/core/form_fields.py b/kuma/core/form_fields.py index 14a45fcb864..68609f29051 100644 --- a/kuma/core/form_fields.py +++ b/kuma/core/form_fields.py @@ -1,5 +1,3 @@ - - from babel import Locale, localedata from babel.support import Format from django import forms @@ -14,18 +12,17 @@ # @see http://code.djangoproject.com/ticket/6362 class StrippedCharField(forms.CharField): """CharField that strips trailing and leading spaces.""" + def __init__(self, max_length=None, min_length=None, *args, **kwargs): self.max_length, self.min_length = max_length, min_length - super(StrippedCharField, self).__init__(max_length, min_length, - *args, **kwargs) + super(StrippedCharField, self).__init__(max_length, min_length, *args, **kwargs) # Remove the default min and max length validators and add our own # that format numbers in the error messages. to_remove = [] for validator in self.validators: class_name = validator.__class__.__name__ - if class_name == 'MinLengthValidator' or \ - class_name == 'MaxLengthValidator': + if class_name == "MinLengthValidator" or class_name == "MaxLengthValidator": to_remove.append(validator) for validator in to_remove: self.validators.remove(validator) @@ -43,26 +40,31 @@ def clean(self, value): class BaseValidator(validators.BaseValidator): """Override the BaseValidator from django to format numbers.""" + def __call__(self, value): cleaned = self.clean(value) - params = {'limit_value': _format_decimal(self.limit_value), - 'show_value': _format_decimal(cleaned)} + params = { + "limit_value": _format_decimal(self.limit_value), + "show_value": _format_decimal(cleaned), + } if self.compare(cleaned, self.limit_value): raise ValidationError( - self.message % params, - code=self.code, - params=params, + self.message % params, code=self.code, params=params, ) class MinLengthValidator(validators.MinLengthValidator, BaseValidator): - message = _('Ensure this value has at least %(limit_value)s ' - 'characters (it has %(show_value)s).') + message = _( + "Ensure this value has at least %(limit_value)s " + "characters (it has %(show_value)s)." + ) class MaxLengthValidator(validators.MaxLengthValidator, BaseValidator): - message = _('Ensure this value has at most %(limit_value)s ' - 'characters (it has %(show_value)s).') + message = _( + "Ensure this value has at most %(limit_value)s " + "characters (it has %(show_value)s)." + ) def _format_decimal(num, format=None): diff --git a/kuma/core/ga_tracking.py b/kuma/core/ga_tracking.py index 8c552134977..780e7ddf9cf 100644 --- a/kuma/core/ga_tracking.py +++ b/kuma/core/ga_tracking.py @@ -7,7 +7,7 @@ from requests.exceptions import RequestException -log = logging.getLogger('kuma.core.ga_tracking') +log = logging.getLogger("kuma.core.ga_tracking") # The `track_event()` function can take any string but to minimize risk of # typos, it's highly recommended to use a constant instead. @@ -33,22 +33,22 @@ # # Here are some of those useful constants... -CATEGORY_SIGNUP_FLOW = 'signup-flow' +CATEGORY_SIGNUP_FLOW = "signup-flow" # Right before redirecting to the auth provider. -ACTION_AUTH_STARTED = 'auth-started' +ACTION_AUTH_STARTED = "auth-started" # When redirected back from auth provider and it worked. -ACTION_AUTH_SUCCESSFUL = 'auth-successful' +ACTION_AUTH_SUCCESSFUL = "auth-successful" # When we don't need to ask the user to create a profile. -ACTION_RETURNING_USER_SIGNIN = 'returning-user-signin' +ACTION_RETURNING_USER_SIGNIN = "returning-user-signin" # Presented with the "Create Profile" form. -ACTION_PROFILE_AUDIT = 'profile-audit' +ACTION_PROFILE_AUDIT = "profile-audit" # Have completed the profile creation form. -ACTION_PROFILE_CREATED = 'profile-created' +ACTION_PROFILE_CREATED = "profile-created" # Checked or didn't check the "Newsletter" checkbox on sign up. -ACTION_FREE_NEWSLETTER = 'free-newsletter' +ACTION_FREE_NEWSLETTER = "free-newsletter" # When logging in with one provider and benefitting from a verified email # existing based on a *different* (already created profile) provider. -ACTION_SOCIAL_AUTH_ADD = 'social-auth-add' +ACTION_SOCIAL_AUTH_ADD = "social-auth-add" def track_event( @@ -75,7 +75,8 @@ def track_event( tracking_url = tracking_url or settings.GOOGLE_ANALYTICS_TRACKING_URL timeout = timeout or settings.GOOGLE_ANALYTICS_TRACKING_TIMEOUT raise_errors = ( - raise_errors if raise_errors is not None + raise_errors + if raise_errors is not None else settings.GOOGLE_ANALYTICS_TRACKING_RAISE_ERRORS ) @@ -84,14 +85,14 @@ def track_event( client_id = client_id or str(uuid.uuid4()) params = { - 'v': '1', - 't': 'event', - 'tid': tracking_id, - 'cid': client_id, - 'ec': event_category, - 'ea': event_action, - 'el': event_label, - 'aip': '1' # anonymize IP + "v": "1", + "t": "event", + "tid": tracking_id, + "cid": client_id, + "ec": event_category, + "ea": event_action, + "el": event_label, + "aip": "1", # anonymize IP } url = f"{tracking_url}?{urlencode(params)}" try: @@ -104,4 +105,5 @@ def track_event( log.error( "Failed sending GA tracking event " f"{event_category!r}, {event_action!r}, {event_label!r}", - exc_info=True) + exc_info=True, + ) diff --git a/kuma/core/i18n.py b/kuma/core/i18n.py index eaf588d0816..7b1779740d9 100644 --- a/kuma/core/i18n.py +++ b/kuma/core/i18n.py @@ -12,8 +12,12 @@ from django.conf.locale import LANG_INFO from django.utils import translation from django.utils.translation.trans_real import ( - check_for_language, get_languages as _django_get_languages, - language_code_prefix_re, language_code_re, parse_accept_lang_header) + check_for_language, + get_languages as _django_get_languages, + language_code_prefix_re, + language_code_re, + parse_accept_lang_header, +) from jinja2 import nodes from jinja2.ext import Extension @@ -51,8 +55,10 @@ def get_django_languages(): This would be the same as Django's get_languages, if we were using Django language codes. """ - return {kuma_language_code_to_django(locale): name - for locale, name in settings.LANGUAGES} + return { + kuma_language_code_to_django(locale): name + for locale, name in settings.LANGUAGES + } def get_kuma_languages(): @@ -97,10 +103,10 @@ def get_supported_language_variant(raw_lang_code): # If 'fr-ca' is not supported, try special fallback or language-only 'fr'. possible_lang_codes = [lang_code] try: - possible_lang_codes.extend(LANG_INFO[lang_code]['fallback']) + possible_lang_codes.extend(LANG_INFO[lang_code]["fallback"]) except KeyError: pass - generic_lang_code = lang_code.split('-')[0] + generic_lang_code = lang_code.split("-")[0] possible_lang_codes.append(generic_lang_code) supported_lang_codes = get_django_languages() @@ -111,7 +117,7 @@ def get_supported_language_variant(raw_lang_code): return django_language_code_to_kuma(code) # If fr-fr is not supported, try fr-ca. for supported_code in supported_lang_codes: - if supported_code.startswith(generic_lang_code + '-'): + if supported_code.startswith(generic_lang_code + "-"): # Kuma: Convert to Kuma language code return django_language_code_to_kuma(supported_code) raise LookupError(raw_lang_code) @@ -174,9 +180,9 @@ def get_language_from_request(request): pass # Pick the closest langauge based on the Accept Language header - accept = request.META.get('HTTP_ACCEPT_LANGUAGE', '') + accept = request.META.get("HTTP_ACCEPT_LANGUAGE", "") for accept_lang, unused in parse_accept_lang_header(accept): - if accept_lang == '*': + if accept_lang == "*": break # Kuma: Assert accept_lang fits the language code pattern @@ -202,7 +208,7 @@ def get_language_from_request(request): def get_language_mapping(): - return apps.get_app_config('core').language_mapping + return apps.get_app_config("core").language_mapping def activate_language_from_request(request): @@ -246,7 +252,8 @@ class TranslationExtension(Extension): http://jinja.pocoo.org/docs/2.10/extensions/#module-jinja2.ext """ - tags = {'translation'} + + tags = {"translation"} def parse(self, parser): """Parse a stream starting with {% translation %}.""" @@ -255,12 +262,12 @@ def parse(self, parser): block_language = parser.parse_expression() # Parse the block body until {% endtranslation %} - body = parser.parse_statements(['name:endtranslation'], - drop_needle=True) + body = parser.parse_statements(["name:endtranslation"], drop_needle=True) # Return a node that will render body in the desired translation - return nodes.CallBlock(self.call_method('_override', [block_language]), - [], [], body).set_lineno(lineno) + return nodes.CallBlock( + self.call_method("_override", [block_language]), [], [], body + ).set_lineno(lineno) def _override(self, block_language, caller): """Render a {% translation %} block with the requested language.""" diff --git a/kuma/core/jobs.py b/kuma/core/jobs.py index 19439e98edb..c5558ac0f96 100644 --- a/kuma/core/jobs.py +++ b/kuma/core/jobs.py @@ -1,5 +1,3 @@ - - from cacheback.base import Job from django.utils import crypto @@ -9,12 +7,13 @@ class KumaJob(Job): A subclass of the cache base job class that implements an optional per job version key. """ + version = None def key(self, *args, **kwargs): key = super(KumaJob, self).key(*args, **kwargs) if self.version is not None: - key = f'{key}#{self.version}' + key = f"{key}#{self.version}" return key @@ -24,6 +23,7 @@ class GenerationJob(KumaJob): The purpose is to refresh several cached values when a generation changes. """ + generation_lifetime = 60 * 60 * 24 * 365 lifetime = 60 * 60 * 12 @@ -38,15 +38,17 @@ def __init__(self, generation_args=None, *args, **kwargs): self.generation_args = generation_args or [] super(KumaJob, self).__init__(*args, **kwargs) self.generation_key = GenerationKeyJob( - lifetime=self.generation_lifetime, for_class=self.class_path, - generation_args=self.generation_args) + lifetime=self.generation_lifetime, + for_class=self.class_path, + generation_args=self.generation_args, + ) def key(self, *args, **kwargs): """Create a key that is derived from the generation.""" base_key = super(GenerationJob, self).key(*args, **kwargs) gen_key = self.generation_key.key() gen_key_value = self.generation_key.get() - return f'{base_key}@{gen_key}:{gen_key_value}' + return f"{base_key}@{gen_key}:{gen_key_value}" def invalidate_generation(self): """Invalidate the shared generation.""" @@ -65,8 +67,8 @@ def __init__(self, lifetime, for_class, generation_args, *args, **kwargs): def key(self, *args, **kwargs): """Return a key that is derived only from the initial args.""" - generation_args = ':'.join([str(key) for key in self.generation_args]) - return f'{self.for_class}:{generation_args}:generation' + generation_args = ":".join([str(key) for key in self.generation_args]) + return f"{self.for_class}:{generation_args}:generation" def fetch(self, *args, **kwargs): """Create a unique generation identifier.""" @@ -78,22 +80,26 @@ def get_init_kwargs(self): The async refresh task re-creates the GenerationKeyJob. """ - return {'lifetime': self.lifetime, - 'for_class': self.for_class, - 'generation_args': self.generation_args} + return { + "lifetime": self.lifetime, + "for_class": self.for_class, + "generation_args": self.generation_args, + } class BannedIPsJob(KumaJob): - '''Get the current set of banned IPs.''' + """Get the current set of banned IPs.""" + lifetime = 60 * 60 * 3 refresh_timeout = 60 def fetch(self): """Get a (JSON-serializable) list of banned IPs.""" from .models import IPBan - ips = list(IPBan.objects - .filter(deleted__isnull=True) - .values_list('ip', flat=True)) + + ips = list( + IPBan.objects.filter(deleted__isnull=True).values_list("ip", flat=True) + ) return ips def empty(self): diff --git a/kuma/core/management/commands/anonymize.py b/kuma/core/management/commands/anonymize.py index abe2fe81bbf..76192b77554 100644 --- a/kuma/core/management/commands/anonymize.py +++ b/kuma/core/management/commands/anonymize.py @@ -1,5 +1,3 @@ - - import os.path from django.conf import settings @@ -8,10 +6,10 @@ class Command(BaseCommand): - help = 'Anonymize the database. Will wipe out some data.' + help = "Anonymize the database. Will wipe out some data." def handle(self, *arg, **kwargs): - path = os.path.join(settings.ROOT, 'scripts/anonymize.sql') + path = os.path.join(settings.ROOT, "scripts/anonymize.sql") sql = open(path).read() assert sql cursor = connection.cursor() diff --git a/kuma/core/management/commands/delete_old_ip_bans.py b/kuma/core/management/commands/delete_old_ip_bans.py index 3bb01b92529..f1cd85acba4 100644 --- a/kuma/core/management/commands/delete_old_ip_bans.py +++ b/kuma/core/management/commands/delete_old_ip_bans.py @@ -13,10 +13,8 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument( - '--days', - help="How many days 'old' (default 30)", - default=30, - type=int) + "--days", help="How many days 'old' (default 30)", default=30, type=int + ) def handle(self, *args, **options): - delete_old_ip_bans(days=options['days']) + delete_old_ip_bans(days=options["days"]) diff --git a/kuma/core/management/commands/ihavepower.py b/kuma/core/management/commands/ihavepower.py index 1a1cfa838db..ade8f3900ac 100644 --- a/kuma/core/management/commands/ihavepower.py +++ b/kuma/core/management/commands/ihavepower.py @@ -1,32 +1,30 @@ - - from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand, CommandError class Command(BaseCommand): - help = 'Converts specified user into a superuser' + help = "Converts specified user into a superuser" def add_arguments(self, parser): - parser.add_argument('username', nargs=1, - help='Username address for account to admin-ize.') + parser.add_argument( + "username", nargs=1, help="Username address for account to admin-ize." + ) - parser.add_argument('--password', default=False, - help='Password of the user') + parser.add_argument("--password", default=False, help="Password of the user") def handle(self, *args, **options): - username = options['username'][0] - password = options['password'] + username = options["username"][0] + password = options["password"] User = get_user_model() try: user = User.objects.get(username=username) except User.DoesNotExist: - raise CommandError('User %s does not exist.' % username) + raise CommandError("User %s does not exist." % username) if user.is_superuser and user.is_staff and not password: - raise CommandError('User already has the power!') + raise CommandError("User already has the power!") if password: user.set_password(password) @@ -34,4 +32,4 @@ def handle(self, *args, **options): user.is_superuser = True user.is_staff = True user.save() - self.stdout.write('Done!') + self.stdout.write("Done!") diff --git a/kuma/core/management/commands/translate_locales_name.py b/kuma/core/management/commands/translate_locales_name.py index efe3a179ced..d7721741786 100644 --- a/kuma/core/management/commands/translate_locales_name.py +++ b/kuma/core/management/commands/translate_locales_name.py @@ -1,5 +1,3 @@ - - from django.conf import settings from django.core.management.base import BaseCommand @@ -7,26 +5,33 @@ # A script to generate a template which will be used to localize the supported locale name # For more information see https://bugzil.la/859499#c11 + class Command(BaseCommand): help = "Generate a template to get the locales name localized" def handle(self, *args, **options): - template_string = ("This template is automatically generated by " - "*manage.py translate_locales_name* in order " - "to make the languages name localizable. " - "Do not edit it manually\n" - "Background: https://bugzil.la/859499#c11\n") - - LANGUAGES = sorted([lang_info.english - for lang_code, lang_info in settings.LOCALES.items() - if lang_code in settings.ENABLED_LOCALES]) + template_string = ( + "This template is automatically generated by " + "*manage.py translate_locales_name* in order " + "to make the languages name localizable. " + "Do not edit it manually\n" + "Background: https://bugzil.la/859499#c11\n" + ) + + LANGUAGES = sorted( + [ + lang_info.english + for lang_code, lang_info in settings.LOCALES.items() + if lang_code in settings.ENABLED_LOCALES + ] + ) for lang in LANGUAGES: template_string += "{{ _('%s') }}\n" % lang jinja_path = settings.TEMPLATES[0]["DIRS"][0] - template_path = jinja_path + '/includes/translate_locales.html' + template_path = jinja_path + "/includes/translate_locales.html" outfile = open(template_path, "w") - outfile.write(template_string.encode('utf8')) + outfile.write(template_string.encode("utf8")) outfile.close() diff --git a/kuma/core/managers.py b/kuma/core/managers.py index 82116a8cc8b..cfb49fec259 100644 --- a/kuma/core/managers.py +++ b/kuma/core/managers.py @@ -29,12 +29,11 @@ def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH): return [(t.id, t.name) for t in Tag.objects.all()] def __init__(self, *args, **kwargs): - kwargs['manager'] = _NamespacedTaggableManager + kwargs["manager"] = _NamespacedTaggableManager super(NamespacedTaggableManager, self).__init__(*args, **kwargs) class _NamespacedTaggableManager(_TaggableManager): - def __str__(self): """Return the list of tags as an editable string. Expensive: Does a DB query for the tags""" @@ -45,9 +44,9 @@ def all_ns(self, namespace=None): """Fetch tags by namespace, or collate all into namespaces""" tags = self.all() - if namespace == '': + if namespace == "": # Empty namespace is special - just look for absence of ':' - return tags.exclude(name__contains=':') + return tags.exclude(name__contains=":") if namespace is not None: # Namespace requested, so generate filtered set @@ -83,7 +82,7 @@ def remove_ns(self, namespace=None, *tags): def clear_ns(self, namespace=None): """Clear tags within a namespace""" lookup_kwargs = self._lookup_kwargs() - lookup_kwargs['tag__name__startswith'] = namespace + lookup_kwargs["tag__name__startswith"] = namespace self.through.objects.filter(**lookup_kwargs).delete() @require_instance_manager @@ -97,18 +96,18 @@ def _parse_ns(self, tag): Namespace is tag name text up to and including the last occurrence of ':' """ - if (':' in tag.name): - (ns, name) = tag.name.rsplit(':', 1) - return ('%s:' % ns, name) + if ":" in tag.name: + (ns, name) = tag.name.rsplit(":", 1) + return ("%s:" % ns, name) else: - return ('', tag.name) + return ("", tag.name) def _ensure_ns(self, namespace, tags): """Ensure each tag name in the list starts with the given namespace""" ns_tags = [] for t in tags: if not t.startswith(namespace): - t = f'{namespace}{t}' + t = f"{namespace}{t}" ns_tags.append(t) return ns_tags diff --git a/kuma/core/middleware.py b/kuma/core/middleware.py index 3307a360144..a2a573633b9 100644 --- a/kuma/core/middleware.py +++ b/kuma/core/middleware.py @@ -3,28 +3,30 @@ from django.conf import settings from django.contrib.sessions.middleware import SessionMiddleware from django.core.exceptions import MiddlewareNotUsed -from django.http import (HttpResponseForbidden, - HttpResponsePermanentRedirect, - HttpResponseRedirect) +from django.http import ( + HttpResponseForbidden, + HttpResponsePermanentRedirect, + HttpResponseRedirect, +) from django.urls import get_script_prefix, resolve, Resolver404 from django.utils.encoding import smart_str from waffle.middleware import WaffleMiddleware -from kuma.wiki.views.legacy import (mindtouch_to_kuma_redirect, - mindtouch_to_kuma_url) +from kuma.wiki.views.legacy import mindtouch_to_kuma_redirect, mindtouch_to_kuma_url from .decorators import add_shared_cache_control -from .i18n import (activate_language_from_request, - get_kuma_languages, - get_language, - get_language_from_path, - get_language_from_request) +from .i18n import ( + activate_language_from_request, + get_kuma_languages, + get_language, + get_language_from_path, + get_language_from_request, +) from .utils import is_untrusted, urlparams from .views import handler403 class MiddlewareBase(object): - def __init__(self, get_response): self.get_response = get_response @@ -38,7 +40,7 @@ class LangSelectorMiddleware(MiddlewareBase): def __call__(self, request): """Redirect if ?lang query parameter is valid.""" - query_lang = request.GET.get('lang') + query_lang = request.GET.get("lang") if not (query_lang and query_lang in get_kuma_languages()): # Invalid or no language requested, so don't redirect. return self.get_response(request) @@ -46,18 +48,19 @@ def __call__(self, request): # Check if the requested language is already embedded in URL language = get_language_from_request(request) script_prefix = get_script_prefix() - lang_prefix = f'{script_prefix}{language}/' + lang_prefix = f"{script_prefix}{language}/" full_path = request.get_full_path() # Includes querystring old_path = urlsplit(full_path).path - new_prefix = f'{script_prefix}{query_lang}/' + new_prefix = f"{script_prefix}{query_lang}/" if full_path.startswith(lang_prefix): new_path = old_path.replace(lang_prefix, new_prefix, 1) else: new_path = old_path.replace(script_prefix, new_prefix, 1) # Redirect to same path with requested language and without ?lang - new_query = dict((smart_str(k), v) for - k, v in request.GET.items() if k != 'lang') + new_query = dict( + (smart_str(k), v) for k, v in request.GET.items() if k != "lang" + ) if new_query: new_path = urlparams(new_path, **new_query) response = HttpResponseRedirect(new_path) @@ -86,7 +89,7 @@ def __call__(self, request): # 404 URLs without locale prefixes should remain 404s return response - literal_from_path = request.path_info.split('/')[1] + literal_from_path = request.path_info.split("/")[1] fixed_locale = None lower_literal = literal_from_path.lower() lower_language = language_from_path.lower() @@ -162,34 +165,29 @@ def process_response(self, request, response): """Add Content-Language, convert some 404s to locale redirects.""" language = get_language() language_from_path = get_language_from_path(request.path_info) - urlconf = getattr(request, 'urlconf', settings.ROOT_URLCONF) + urlconf = getattr(request, "urlconf", settings.ROOT_URLCONF) # Kuma: assume locale-prefix patterns, including default language if response.status_code == 404 and not language_from_path: # Maybe the language code is missing in the URL? Try adding the # language prefix and redirecting to that URL. - language_path = f'/{language}{request.path_info}' + language_path = f"/{language}{request.path_info}" path_valid = is_valid_path(language_path, language, urlconf) - path_needs_slash = ( - not path_valid and ( - settings.APPEND_SLASH and not language_path.endswith('/') and - is_valid_path('%s/' % language_path, language, urlconf) - ) + path_needs_slash = not path_valid and ( + settings.APPEND_SLASH + and not language_path.endswith("/") + and is_valid_path("%s/" % language_path, language, urlconf) ) if path_valid or path_needs_slash: script_prefix = get_script_prefix() # Insert language after the script prefix and before the # rest of the URL - language_url = ( - request.get_full_path(force_append_slash=path_needs_slash) - .replace( - script_prefix, - f'{script_prefix}{language}/', - 1 - )) + language_url = request.get_full_path( + force_append_slash=path_needs_slash + ).replace(script_prefix, f"{script_prefix}{language}/", 1) # Kuma: Add caching headers to redirect - if request.path_info == '/': + if request.path_info == "/": # Only the homepage should be redirected permanently. redirect = HttpResponsePermanentRedirect(language_url) else: @@ -206,8 +204,8 @@ def process_response(self, request, response): # could be replaced with an assertion, but that would deviate from # Django's version, and make the code brittle, so using a pragma # instead. And a long comment. - if 'Content-Language' not in response: # pragma: no cover - response['Content-Language'] = language + if "Content-Language" not in response: # pragma: no cover + response["Content-Language"] = language return response @@ -242,8 +240,7 @@ def is_valid_path(path, language_code, urlconf=None): if match.func == mindtouch_to_kuma_redirect: # mindtouch_to_kuma_redirect matches everything. # Check if it would return a redirect or 404. - url = mindtouch_to_kuma_url(language_code, - match.kwargs['path']) + url = mindtouch_to_kuma_url(language_code, match.kwargs["path"]) return bool(url) else: return True @@ -269,18 +266,18 @@ class SlashMiddleware(MiddlewareBase): def __call__(self, request): response = self.get_response(request) path = request.path_info - language = getattr(request, 'LANGUAGE_CODE') or settings.LANGUAGE_CODE + language = getattr(request, "LANGUAGE_CODE") or settings.LANGUAGE_CODE if response.status_code == 404 and not is_valid_path(path, language): new_path = None - if path.endswith('/') and is_valid_path(path[:-1], language): + if path.endswith("/") and is_valid_path(path[:-1], language): # Remove the trailing slash for a valid URL new_path = path[:-1] - elif not path.endswith('/') and is_valid_path(path + '/', language): + elif not path.endswith("/") and is_valid_path(path + "/", language): # Add a trailing slash for a valid URL - new_path = path + '/' + new_path = path + "/" if new_path: if request.GET: - new_path += '?' + request.META['QUERY_STRING'] + new_path += "?" + request.META["QUERY_STRING"] return HttpResponsePermanentRedirect(new_path) return response @@ -294,20 +291,19 @@ class SetRemoteAddrFromForwardedFor(MiddlewareBase): def __call__(self, request): try: - forwarded_for = request.META['HTTP_X_FORWARDED_FOR'] + forwarded_for = request.META["HTTP_X_FORWARDED_FOR"] except KeyError: pass else: # HTTP_X_FORWARDED_FOR can be a comma-separated list of IPs. # The client's IP will be the first one. - forwarded_for = forwarded_for.split(',')[0].strip() - request.META['REMOTE_ADDR'] = forwarded_for + forwarded_for = forwarded_for.split(",")[0].strip() + request.META["REMOTE_ADDR"] = forwarded_for return self.get_response(request) class ForceAnonymousSessionMiddleware(SessionMiddleware): - def process_request(self, request): """ Always create an anonymous session. @@ -331,7 +327,7 @@ def __init__(self, get_response): def __call__(self, request): if is_untrusted(request): - request.urlconf = 'kuma.urls_untrusted' + request.urlconf = "kuma.urls_untrusted" return self.get_response(request) @@ -344,13 +340,15 @@ class is a simple wrapper around the waffle.middleware.WaffleMiddleware by waffle.middleware.WaffleMiddleware. The domain is set to the value configured in settings.WAFFLE_COOKIE_DOMAIN. """ + def process_response(self, request, response): keys_before = frozenset(response.cookies.keys()) try: - response = super(WaffleWithCookieDomainMiddleware, - self).process_response(request, response) + response = super(WaffleWithCookieDomainMiddleware, self).process_response( + request, response + ) finally: keys_after = frozenset(response.cookies.keys()) - for key in (keys_after - keys_before): - response.cookies[key]['domain'] = settings.WAFFLE_COOKIE_DOMAIN + for key in keys_after - keys_before: + response.cookies[key]["domain"] = settings.WAFFLE_COOKIE_DOMAIN return response diff --git a/kuma/core/migrations/0001_squashed_0004_remove_unused_tags.py b/kuma/core/migrations/0001_squashed_0004_remove_unused_tags.py index 162bb2e1e35..7303ae92c26 100644 --- a/kuma/core/migrations/0001_squashed_0004_remove_unused_tags.py +++ b/kuma/core/migrations/0001_squashed_0004_remove_unused_tags.py @@ -1,5 +1,3 @@ - - from django.db import migrations, models import django.utils.timezone @@ -7,17 +5,30 @@ class Migration(migrations.Migration): dependencies = [ - ('taggit', '0002_auto_20150616_2121'), + ("taggit", "0002_auto_20150616_2121"), ] operations = [ migrations.CreateModel( - name='IPBan', + name="IPBan", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('ip', models.GenericIPAddressField()), - ('created', models.DateTimeField(default=django.utils.timezone.now, db_index=True)), - ('deleted', models.DateTimeField(null=True, blank=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("ip", models.GenericIPAddressField()), + ( + "created", + models.DateTimeField( + default=django.utils.timezone.now, db_index=True + ), + ), + ("deleted", models.DateTimeField(null=True, blank=True)), ], ), ] diff --git a/kuma/core/migrations/0002_auto_20191206_0805.py b/kuma/core/migrations/0002_auto_20191206_0805.py index 35330ba2ebe..609de34ddd3 100644 --- a/kuma/core/migrations/0002_auto_20191206_0805.py +++ b/kuma/core/migrations/0002_auto_20191206_0805.py @@ -6,9 +6,9 @@ def move_developer_needs_flag_to_switch(apps, schema_editor): - Flag = apps.get_model('waffle', 'Flag') - Switch = apps.get_model('waffle', 'Switch') - name = 'developer_needs' + Flag = apps.get_model("waffle", "Flag") + Switch = apps.get_model("waffle", "Switch") + name = "developer_needs" for flag in Flag.objects.filter(name=name): active = flag.everyone or (flag.percent and flag.percent > 0) Switch.objects.get_or_create(name=name, active=active, note=flag.note) @@ -18,12 +18,10 @@ def move_developer_needs_flag_to_switch(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('core', '0001_squashed_0004_remove_unused_tags'), + ("core", "0001_squashed_0004_remove_unused_tags"), # This is needed otherwise `apps.get_model('waffle', 'Flag')` # will raise a Django app LookupError. - ('waffle', '0001_initial'), + ("waffle", "0001_initial"), ] - operations = [ - migrations.RunPython(move_developer_needs_flag_to_switch) - ] + operations = [migrations.RunPython(move_developer_needs_flag_to_switch)] diff --git a/kuma/core/migrations/0003_auto_20191212_0638.py b/kuma/core/migrations/0003_auto_20191212_0638.py index d3bf471fb74..e37d0135039 100644 --- a/kuma/core/migrations/0003_auto_20191212_0638.py +++ b/kuma/core/migrations/0003_auto_20191212_0638.py @@ -6,19 +6,17 @@ def remove_bcd_signal_waffle_flag(apps, schema_editor): - Flag = apps.get_model('waffle', 'Flag') - Flag.objects.filter(name='bc-signals').delete() + Flag = apps.get_model("waffle", "Flag") + Flag.objects.filter(name="bc-signals").delete() class Migration(migrations.Migration): dependencies = [ - ('core', '0002_auto_20191206_0805'), + ("core", "0002_auto_20191206_0805"), # This is needed otherwise `apps.get_model('waffle', 'Flag')` # will raise a Django app LookupError. - ('waffle', '0001_initial'), + ("waffle", "0001_initial"), ] - operations = [ - migrations.RunPython(remove_bcd_signal_waffle_flag) - ] + operations = [migrations.RunPython(remove_bcd_signal_waffle_flag)] diff --git a/kuma/core/models.py b/kuma/core/models.py index 9f00734b403..741c9615126 100644 --- a/kuma/core/models.py +++ b/kuma/core/models.py @@ -1,5 +1,3 @@ - - from django.db import models from django.dispatch import receiver from django.utils import timezone @@ -14,6 +12,7 @@ class IPBan(models.Model): Currently, this only bans an IP address from editing. """ + ip = models.GenericIPAddressField() created = models.DateTimeField(default=timezone.now, db_index=True) deleted = models.DateTimeField(null=True, blank=True) @@ -25,7 +24,7 @@ def delete(self, *args, **kwargs): self.save() def __str__(self): - return f'{self.ip} banned on {self.created}' + return f"{self.ip} banned on {self.created}" @receiver(models.signals.post_save, sender=IPBan) diff --git a/kuma/core/pipeline/cleancss.py b/kuma/core/pipeline/cleancss.py index 4a482641b14..5148416eaee 100644 --- a/kuma/core/pipeline/cleancss.py +++ b/kuma/core/pipeline/cleancss.py @@ -1,13 +1,11 @@ - - from django.conf import settings from pipeline.compressors import SubProcessCompressor class CleanCSSCompressor(SubProcessCompressor): def compress_css(self, css): - binary = settings.PIPELINE.get('CLEANCSS_BINARY', 'cleancss') - args = settings.PIPELINE.get('CLEANCSS_ARGUMENTS', '') + binary = settings.PIPELINE.get("CLEANCSS_BINARY", "cleancss") + args = settings.PIPELINE.get("CLEANCSS_ARGUMENTS", "") # If the arguments ever include quoted arguments with spaces then # the simple split() call here is not going to be good enough. command = (binary,) + tuple(args.split()) diff --git a/kuma/core/pipeline/sass.py b/kuma/core/pipeline/sass.py index d296e5e533a..cf645f29512 100644 --- a/kuma/core/pipeline/sass.py +++ b/kuma/core/pipeline/sass.py @@ -1,5 +1,3 @@ - - from pipeline.compilers.sass import SASSCompiler diff --git a/kuma/core/pipeline/storage.py b/kuma/core/pipeline/storage.py index c24988a7188..816d3d55a04 100644 --- a/kuma/core/pipeline/storage.py +++ b/kuma/core/pipeline/storage.py @@ -1,5 +1,3 @@ - - from django.contrib.staticfiles.storage import ManifestStaticFilesStorage from pipeline.storage import PipelineMixin diff --git a/kuma/core/tasks.py b/kuma/core/tasks.py index eeaa6f64754..944cb312480 100644 --- a/kuma/core/tasks.py +++ b/kuma/core/tasks.py @@ -1,5 +1,3 @@ - - from celery.task import task from constance import config from django.contrib.sessions.models import Session @@ -11,13 +9,12 @@ from .models import IPBan -LOCK_ID = 'clean-sessions-lock' +LOCK_ID = "clean-sessions-lock" LOCK_EXPIRE = 60 * 5 def get_expired_sessions(now): - return (Session.objects.filter(expire_date__lt=now) - .order_by('expire_date')) + return Session.objects.filter(expire_date__lt=now).order_by("expire_date") @task @@ -30,29 +27,34 @@ def clean_sessions(): logger = clean_sessions.get_logger() chunk_size = config.SESSION_CLEANUP_CHUNK_SIZE - if cache.add(LOCK_ID, now.strftime('%c'), LOCK_EXPIRE): + if cache.add(LOCK_ID, now.strftime("%c"), LOCK_EXPIRE): total_count = get_expired_sessions(now).count() delete_count = 0 - logger.info('Deleting the %s of %s oldest expired sessions' % - (chunk_size, total_count)) + logger.info( + "Deleting the %s of %s oldest expired sessions" % (chunk_size, total_count) + ) try: cursor = connection.cursor() - delete_count = cursor.execute(""" + delete_count = cursor.execute( + """ DELETE FROM django_session WHERE expire_date < NOW() ORDER BY expire_date ASC LIMIT %s; - """, [chunk_size]) + """, + [chunk_size], + ) finally: - logger.info('Deleted %s expired sessions' % delete_count) + logger.info("Deleted %s expired sessions" % delete_count) cache.delete(LOCK_ID) expired_sessions = get_expired_sessions(now) if expired_sessions.exists(): clean_sessions.apply_async() else: - logger.error('The clean_sessions task is already running since %s' % - cache.get(LOCK_ID)) + logger.error( + "The clean_sessions task is already running since %s" % cache.get(LOCK_ID) + ) @task diff --git a/kuma/core/templatetags/jinja_helpers.py b/kuma/core/templatetags/jinja_helpers.py index 92aaa9d9ed7..311996e808c 100644 --- a/kuma/core/templatetags/jinja_helpers.py +++ b/kuma/core/templatetags/jinja_helpers.py @@ -1,5 +1,3 @@ - - import datetime import html import json @@ -20,8 +18,7 @@ from urlobject import URLObject from ..urlresolvers import reverse, split_path -from ..utils import (format_date_time, is_untrusted, is_wiki, order_params, - urlparams) +from ..utils import format_date_time, is_untrusted, is_wiki, order_params, urlparams # Yanking filters from Django. @@ -37,15 +34,15 @@ library.global_function(is_untrusted) -@library.global_function(name='assert') +@library.global_function(name="assert") def assert_function(statement, message=None): """Add runtime assertions to Jinja2 templates.""" if not statement: if message: - raise RuntimeError('Failed assertion: {}'.format(message)) + raise RuntimeError("Failed assertion: {}".format(message)) else: - raise RuntimeError('Failed assertion') - return '' + raise RuntimeError("Failed assertion") + return "" @library.filter @@ -57,12 +54,11 @@ def paginator(pager): @library.global_function def url(viewname, *args, **kwargs): """Helper for Django's ``reverse`` in templates.""" - locale = kwargs.pop('locale', None) + locale = kwargs.pop("locale", None) return reverse(viewname, args=args, kwargs=kwargs, locale=locale) class Paginator(object): - def __init__(self, pager): self.pager = pager @@ -91,15 +87,14 @@ def range(self): return range(max(lower + 1, 1), min(total, upper) + 1) def render(self): - c = {'pager': self.pager, 'num_pages': self.num_pages, - 'count': self.count} - t = get_template('includes/paginator.html').render(c) + c = {"pager": self.pager, "num_pages": self.num_pages, "count": self.count} + t = get_template("includes/paginator.html").render(c) return jinja2.Markup(t) @library.filter def yesno(boolean_value): - return jinja2.Markup(_('Yes') if boolean_value else _('No')) + return jinja2.Markup(_("Yes") if boolean_value else _("No")) @library.filter @@ -110,13 +105,14 @@ def entity_decode(str): @library.global_function def page_title(title): - return jinja2.Markup('%s | MDN' % jinja2.escape(title)) + return jinja2.Markup("%s | MDN" % jinja2.escape(title)) @library.filter def level_tag(message): - return jinja2.Markup(force_text(LEVEL_TAGS.get(message.level, ''), - strings_only=True)) + return jinja2.Markup( + force_text(LEVEL_TAGS.get(message.level, ""), strings_only=True) + ) @library.global_function @@ -137,35 +133,35 @@ def get_soapbox_messages(url): @library.global_function -@library.render_with('core/elements/soapbox_messages.html') +@library.render_with("core/elements/soapbox_messages.html") def soapbox_messages(soapbox_messages): - return {'soapbox_messages': soapbox_messages} + return {"soapbox_messages": soapbox_messages} @library.global_function -def add_utm(url_, campaign, source='developer.mozilla.org', medium='email'): +def add_utm(url_, campaign, source="developer.mozilla.org", medium="email"): """Add the utm_* tracking parameters to a URL.""" - url_obj = URLObject(url_).add_query_params({ - 'utm_campaign': campaign, - 'utm_source': source, - 'utm_medium': medium}) + url_obj = URLObject(url_).add_query_params( + {"utm_campaign": campaign, "utm_source": source, "utm_medium": medium} + ) return order_params(str(url_obj)) @library.global_function @jinja2.contextfunction -def datetimeformat(context, value, format='shortdatetime', output='html'): +def datetimeformat(context, value, format="shortdatetime", output="html"): """ Returns date/time formatted using babel's locale settings. Uses the timezone from settings.py """ - request = context['request'] + request = context["request"] formatted, tzvalue = format_date_time(request, value, format) - if output == 'json': + if output == "json": return formatted - return jinja2.Markup('' % - (tzvalue.isoformat(), formatted)) + return jinja2.Markup( + '' % (tzvalue.isoformat(), formatted) + ) @library.global_function diff --git a/kuma/core/tests/__init__.py b/kuma/core/tests/__init__.py index 8275ac77433..dfd4eb53aaa 100644 --- a/kuma/core/tests/__init__.py +++ b/kuma/core/tests/__init__.py @@ -1,5 +1,3 @@ - - import os from functools import wraps from unittest import mock @@ -14,23 +12,23 @@ def assert_redirect_to_wiki(response, url): assert response.status_code == 301 - assert response['Location'].endswith(settings.WIKI_HOST + url) + assert response["Location"].endswith(settings.WIKI_HOST + url) def assert_no_cache_header(response): - assert 'max-age=0' in response['Cache-Control'] - assert 'no-cache' in response['Cache-Control'] - assert 'no-store' in response['Cache-Control'] - assert 'must-revalidate' in response['Cache-Control'] - assert 's-maxage' not in response['Cache-Control'] + assert "max-age=0" in response["Cache-Control"] + assert "no-cache" in response["Cache-Control"] + assert "no-store" in response["Cache-Control"] + assert "must-revalidate" in response["Cache-Control"] + assert "s-maxage" not in response["Cache-Control"] def assert_shared_cache_header(response): - assert 'public' in response['Cache-Control'] - assert 's-maxage' in response['Cache-Control'] + assert "public" in response["Cache-Control"] + assert "s-maxage" in response["Cache-Control"] -def get_user(username='testuser'): +def get_user(username="testuser"): """Return a django user or raise FixtureMissingError""" User = get_user_model() return User.objects.get(username=username) @@ -61,10 +59,10 @@ def get_messages(self, request): return messages def assertFileExists(self, path): - self.assertTrue(os.path.exists(path), 'Path %r does not exist' % path) + self.assertTrue(os.path.exists(path), "Path %r does not exist" % path) def assertFileNotExists(self, path): - self.assertFalse(os.path.exists(path), 'Path %r does exist' % path) + self.assertFalse(os.path.exists(path), "Path %r does exist" % path) class KumaTestCase(KumaTestMixin, TestCase): @@ -106,7 +104,7 @@ def run_immediately(some_callable): @wraps(test_method) def inner(*args, **kwargs): - with mock.patch('django.db.transaction.on_commit') as mocker: + with mock.patch("django.db.transaction.on_commit") as mocker: mocker.side_effect = run_immediately return test_method(*args, **kwargs) diff --git a/kuma/core/tests/logging_urls.py b/kuma/core/tests/logging_urls.py index db78d6c1268..df324a9b3d4 100644 --- a/kuma/core/tests/logging_urls.py +++ b/kuma/core/tests/logging_urls.py @@ -1,5 +1,3 @@ - - from django.conf.urls import url from django.core.exceptions import SuspiciousOperation @@ -7,11 +5,7 @@ def suspicious(request): - raise SuspiciousOperation('Raising exception to test logging.') + raise SuspiciousOperation("Raising exception to test logging.") -urlpatterns = i18n_patterns( - url(r'^suspicious/$', - suspicious, - name='suspicious') -) +urlpatterns = i18n_patterns(url(r"^suspicious/$", suspicious, name="suspicious")) diff --git a/kuma/core/tests/taggit_extras/models.py b/kuma/core/tests/taggit_extras/models.py index d50ff929408..6b7de1d0acd 100644 --- a/kuma/core/tests/taggit_extras/models.py +++ b/kuma/core/tests/taggit_extras/models.py @@ -1,5 +1,3 @@ - - from django.db import models from ...managers import NamespacedTaggableManager diff --git a/kuma/core/tests/test_backends.py b/kuma/core/tests/test_backends.py index da5ce85700f..9f48cabfe52 100644 --- a/kuma/core/tests/test_backends.py +++ b/kuma/core/tests/test_backends.py @@ -11,21 +11,21 @@ def test_read_only_constance_db_backend(): from constance import settings - with patch.object(settings, 'DATABASE_CACHE_BACKEND', new=None): + with patch.object(settings, "DATABASE_CACHE_BACKEND", new=None): rw_be = DatabaseBackend() - with patch.object(models.Model, 'save') as save_mock: - rw_be.set('x', 3) + with patch.object(models.Model, "save") as save_mock: + rw_be.set("x", 3) assert save_mock.called - rw_be.set('x', 7) - assert rw_be.get('x') == 7 + rw_be.set("x", 7) + assert rw_be.get("x") == 7 ro_be = ReadOnlyConstanceDatabaseBackend() - with patch.object(models.Model, 'save') as save_mock: - ro_be.set('y', 3) + with patch.object(models.Model, "save") as save_mock: + ro_be.set("y", 3) assert not save_mock.called - ro_be.set('y', 3) - assert ro_be.get('y') is None - assert ro_be.get('x') == 7 + ro_be.set("y", 3) + assert ro_be.get("y") is None + assert ro_be.get("x") == 7 diff --git a/kuma/core/tests/test_commands.py b/kuma/core/tests/test_commands.py index a40a4fc942d..2f96c31dd45 100644 --- a/kuma/core/tests/test_commands.py +++ b/kuma/core/tests/test_commands.py @@ -6,22 +6,22 @@ def test_help(): with pytest.raises(CommandError) as excinfo: - call_command('ihavepower', stdout=StringIO()) + call_command("ihavepower", stdout=StringIO()) - assert str(excinfo.value) == 'Error: the following arguments are required: username' + assert str(excinfo.value) == "Error: the following arguments are required: username" def test_user_doesnt_exist(db): with pytest.raises(CommandError) as excinfo: - call_command('ihavepower', 'fordprefect', stdout=StringIO()) + call_command("ihavepower", "fordprefect", stdout=StringIO()) - assert str(excinfo.value) == 'User fordprefect does not exist.' + assert str(excinfo.value) == "User fordprefect does not exist." def test_user_exists(wiki_user): assert wiki_user.is_staff is False assert wiki_user.is_superuser is False - call_command('ihavepower', wiki_user.username, stdout=StringIO()) + call_command("ihavepower", wiki_user.username, stdout=StringIO()) wiki_user.refresh_from_db() assert wiki_user.is_staff is True assert wiki_user.is_superuser is True diff --git a/kuma/core/tests/test_decorators.py b/kuma/core/tests/test_decorators.py index 63ebebbef86..78946d08f1d 100644 --- a/kuma/core/tests/test_decorators.py +++ b/kuma/core/tests/test_decorators.py @@ -1,5 +1,3 @@ - - import pytest from django.contrib.auth.models import AnonymousUser @@ -10,11 +8,15 @@ from kuma.users.tests import UserTestCase from . import assert_no_cache_header, KumaTestCase -from ..decorators import (block_user_agents, login_required, - logout_required, permission_required, - redirect_in_maintenance_mode, - shared_cache_control, - skip_in_maintenance_mode) +from ..decorators import ( + block_user_agents, + login_required, + logout_required, + permission_required, + redirect_in_maintenance_mode, + shared_cache_control, + skip_in_maintenance_mode, +) def simple_view(request): @@ -25,33 +27,33 @@ class LogoutRequiredTestCase(UserTestCase): rf = RequestFactory() def test_logged_out_default(self): - request = self.rf.get('/foo') + request = self.rf.get("/foo") request.user = AnonymousUser() view = logout_required(simple_view) response = view(request) assert 200 == response.status_code def test_logged_in_default(self): - request = self.rf.get('/foo') - request.user = self.user_model.objects.get(username='testuser') + request = self.rf.get("/foo") + request.user = self.user_model.objects.get(username="testuser") view = logout_required(simple_view) response = view(request) assert 302 == response.status_code def test_logged_in_argument(self): - request = self.rf.get('/foo') - request.user = self.user_model.objects.get(username='testuser') - view = logout_required('/bar')(simple_view) + request = self.rf.get("/foo") + request.user = self.user_model.objects.get(username="testuser") + view = logout_required("/bar")(simple_view) response = view(request) assert 302 == response.status_code - assert '/bar' == response['location'] + assert "/bar" == response["location"] class LoginRequiredTestCase(UserTestCase): rf = RequestFactory() def test_logged_out_default(self): - request = self.rf.get('/foo') + request = self.rf.get("/foo") request.user = AnonymousUser() view = login_required(simple_view) response = view(request) @@ -59,16 +61,16 @@ def test_logged_out_default(self): def test_logged_in_default(self): """Active user login.""" - request = self.rf.get('/foo') - request.user = self.user_model.objects.get(username='testuser') + request = self.rf.get("/foo") + request.user = self.user_model.objects.get(username="testuser") view = login_required(simple_view) response = view(request) assert 200 == response.status_code def test_logged_in_inactive(self): """Inactive user login not allowed by default.""" - request = self.rf.get('/foo') - user = self.user_model.objects.get(username='testuser2') + request = self.rf.get("/foo") + user = self.user_model.objects.get(username="testuser2") user.is_active = False request.user = user view = login_required(simple_view) @@ -77,8 +79,8 @@ def test_logged_in_inactive(self): def test_logged_in_inactive_allow(self): """Inactive user login explicitly allowed.""" - request = self.rf.get('/foo') - user = self.user_model.objects.get(username='testuser2') + request = self.rf.get("/foo") + user = self.user_model.objects.get(username="testuser2") user.is_active = False request.user = user view = login_required(simple_view, only_active=False) @@ -90,59 +92,63 @@ class PermissionRequiredTestCase(UserTestCase): rf = RequestFactory() def test_logged_out_default(self): - request = self.rf.get('/foo') + request = self.rf.get("/foo") request.user = AnonymousUser() - view = permission_required('perm')(simple_view) + view = permission_required("perm")(simple_view) response = view(request) assert 302 == response.status_code def test_logged_in_default(self): - request = self.rf.get('/foo') - request.user = self.user_model.objects.get(username='testuser') - view = permission_required('perm')(simple_view) + request = self.rf.get("/foo") + request.user = self.user_model.objects.get(username="testuser") + view = permission_required("perm")(simple_view) response = view(request) assert 403 == response.status_code def test_logged_in_inactive(self): """Inactive user is denied access.""" - request = self.rf.get('/foo') - user = self.user_model.objects.get(username='admin') + request = self.rf.get("/foo") + user = self.user_model.objects.get(username="admin") user.is_active = False request.user = user - view = permission_required('perm')(simple_view) + view = permission_required("perm")(simple_view) response = view(request) assert 403 == response.status_code def test_logged_in_admin(self): - request = self.rf.get('/foo') - request.user = self.user_model.objects.get(username='admin') - view = permission_required('perm')(simple_view) + request = self.rf.get("/foo") + request.user = self.user_model.objects.get(username="admin") + view = permission_required("perm")(simple_view) response = view(request) assert 200 == response.status_code class TestBlockUserAgents(KumaTestCase): - def setUp(self): - self.request = RequestFactory().get('/foo') + self.request = RequestFactory().get("/foo") def test_regular_agent_ok(self): - self.request.META['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 6.3;' \ - 'WOW64; rv:40.0) Gecko/20100101 Firefox/40.0' + self.request.META["HTTP_USER_AGENT"] = ( + "Mozilla/5.0 (Windows NT 6.3;" "WOW64; rv:40.0) Gecko/20100101 Firefox/40.0" + ) self.view = block_user_agents(simple_view) response = self.view(self.request) assert 200 == response.status_code def test_blocked_agents_forbidden(self): - self.request.META['HTTP_USER_AGENT'] = 'curl/7.21.4 ' \ - '(universal-apple-darwin11.0) libcurl/7.21.4 OpenSSL/0.9.8r ' \ - 'zlib/1.2.5' + self.request.META["HTTP_USER_AGENT"] = ( + "curl/7.21.4 " + "(universal-apple-darwin11.0) libcurl/7.21.4 OpenSSL/0.9.8r " + "zlib/1.2.5" + ) self.view = block_user_agents(simple_view) response = self.view(self.request) assert 403 == response.status_code - self.request.META['HTTP_USER_AGENT'] = 'Mozilla/5.0 (compatible; ' \ - 'Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)' + self.request.META["HTTP_USER_AGENT"] = ( + "Mozilla/5.0 (compatible; " + "Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)" + ) self.view = block_user_agents(simple_view) response = self.view(self.request) assert 403 == response.status_code @@ -150,7 +156,6 @@ def test_blocked_agents_forbidden(self): @pytest.mark.parametrize("maintenance_mode", [False, True]) def test_skip_in_maintenance_mode(settings, maintenance_mode): - @skip_in_maintenance_mode def func(*args, **kwargs): return (args, sorted(kwargs.items())) @@ -160,27 +165,29 @@ def func(*args, **kwargs): if maintenance_mode: assert func(1, 2, x=3, y=4) is None else: - assert func(1, 2, x=3, y=4) == ((1, 2), [('x', 3), ('y', 4)]) + assert func(1, 2, x=3, y=4) == ((1, 2), [("x", 3), ("y", 4)]) @pytest.mark.parametrize( "maintenance_mode, request_method, methods, expected_status_code", - [(True, 'get', None, 302), - (True, 'post', None, 302), - (False, 'get', None, 200), - (False, 'post', None, 200), - (True, 'get', ('PUT', 'POST'), 200), - (True, 'put', ('PUT', 'POST'), 302), - (True, 'post', ('PUT', 'POST'), 302), - (False, 'get', ('PUT', 'POST'), 200), - (False, 'put', ('PUT', 'POST'), 200), - (False, 'post', ('PUT', 'POST'), 200), - (False, 'post', ('PUT', 'POST'), 200)] + [ + (True, "get", None, 302), + (True, "post", None, 302), + (False, "get", None, 200), + (False, "post", None, 200), + (True, "get", ("PUT", "POST"), 200), + (True, "put", ("PUT", "POST"), 302), + (True, "post", ("PUT", "POST"), 302), + (False, "get", ("PUT", "POST"), 200), + (False, "put", ("PUT", "POST"), 200), + (False, "post", ("PUT", "POST"), 200), + (False, "post", ("PUT", "POST"), 200), + ], ) -def test_redirect_in_maintenance_mode_decorator(rf, settings, maintenance_mode, - request_method, methods, - expected_status_code): - request = getattr(rf, request_method)('/foo') +def test_redirect_in_maintenance_mode_decorator( + rf, settings, maintenance_mode, request_method, methods, expected_status_code +): + request = getattr(rf, request_method)("/foo") settings.MAINTENANCE_MODE = maintenance_mode if methods is None: deco = redirect_in_maintenance_mode @@ -192,29 +199,29 @@ def test_redirect_in_maintenance_mode_decorator(rf, settings, maintenance_mode, def test_shared_cache_control_decorator_with_defaults(rf, settings): settings.CACHE_CONTROL_DEFAULT_SHARED_MAX_AGE = 777 - request = rf.get('/foo') + request = rf.get("/foo") response = shared_cache_control(simple_view)(request) assert response.status_code == 200 - assert 'public' in response['Cache-Control'] - assert 'max-age=0' in response['Cache-Control'] - assert 's-maxage=777' in response['Cache-Control'] + assert "public" in response["Cache-Control"] + assert "max-age=0" in response["Cache-Control"] + assert "s-maxage=777" in response["Cache-Control"] def test_shared_cache_control_decorator_with_overrides(rf, settings): settings.CACHE_CONTROL_DEFAULT_SHARED_MAX_AGE = 777 - request = rf.get('/foo') + request = rf.get("/foo") deco = shared_cache_control(max_age=999, s_maxage=0) response = deco(simple_view)(request) assert response.status_code == 200 - assert 'public' in response['Cache-Control'] - assert 'max-age=999' in response['Cache-Control'] - assert 's-maxage=0' in response['Cache-Control'] + assert "public" in response["Cache-Control"] + assert "max-age=999" in response["Cache-Control"] + assert "s-maxage=0" in response["Cache-Control"] def test_shared_cache_control_decorator_keeps_no_cache(rf, settings): - request = rf.get('/foo') + request = rf.get("/foo") response = shared_cache_control(never_cache(simple_view))(request) assert response.status_code == 200 - assert 'public' not in response['Cache-Control'] - assert 's-maxage' not in response['Cache-Control'] + assert "public" not in response["Cache-Control"] + assert "s-maxage" not in response["Cache-Control"] assert_no_cache_header(response) diff --git a/kuma/core/tests/test_form_fields.py b/kuma/core/tests/test_form_fields.py index 19824532186..8364d3792c8 100644 --- a/kuma/core/tests/test_form_fields.py +++ b/kuma/core/tests/test_form_fields.py @@ -1,5 +1,3 @@ - - from django.utils import translation from . import KumaTestCase @@ -7,31 +5,30 @@ class TestFormatDecimal(KumaTestCase): - def test_default_locale(self): """Default locale just works""" num = _format_decimal(1234.567) - assert '1,234.567' == num + assert "1,234.567" == num def test_fr_locale(self): """French locale returns french format""" - translation.activate('fr') + translation.activate("fr") num = _format_decimal(1234.567) - assert '1\u202f234,567' == num + assert "1\u202f234,567" == num def test_xx_YY_locale(self): """Falls back to English for unknown Django locales""" - translation.activate('xx-YY') + translation.activate("xx-YY") # Note: this activation does not make Django attempt to use xx-YY - assert 'xx-yy' == translation.get_language() + assert "xx-yy" == translation.get_language() num = _format_decimal(1234.567) - assert '1,234.567' == num + assert "1,234.567" == num def test_pt_BR_locale(self): """Falls back to English for unknown babel locales""" # Note: if this starts to fail for no apparent reason, it's probably # because babel learned about pt-BR since this test was written. - translation.activate('pt-BR') - assert 'pt-br' == translation.get_language() + translation.activate("pt-BR") + assert "pt-br" == translation.get_language() num = _format_decimal(1234.567) - assert '1,234.567' == num + assert "1,234.567" == num diff --git a/kuma/core/tests/test_ga_tracking.py b/kuma/core/tests/test_ga_tracking.py index 91108fddfe8..923b2933367 100644 --- a/kuma/core/tests/test_ga_tracking.py +++ b/kuma/core/tests/test_ga_tracking.py @@ -5,37 +5,39 @@ def test_happy_path(settings, mock_requests): - settings.GOOGLE_ANALYTICS_ACCOUNT = 'UA-XXXX-1' - mock_requests.register_uri('POST', settings.GOOGLE_ANALYTICS_TRACKING_URL) - track_event('category', 'action', 'label') + settings.GOOGLE_ANALYTICS_ACCOUNT = "UA-XXXX-1" + mock_requests.register_uri("POST", settings.GOOGLE_ANALYTICS_TRACKING_URL) + track_event("category", "action", "label") def test_nothing_happens_if_no_ga_account(settings, mock_requests): # Sanity check fixtures assert not settings.GOOGLE_ANALYTICS_ACCOUNT # This would raise NoMockAddress if it did something. - track_event('category', 'action', 'label') + track_event("category", "action", "label") def test_errors_swallowed(settings, mock_requests): - settings.GOOGLE_ANALYTICS_ACCOUNT = 'UA-XXXX-1' + settings.GOOGLE_ANALYTICS_ACCOUNT = "UA-XXXX-1" settings.GOOGLE_ANALYTICS_TRACKING_RAISE_ERRORS = False mock_requests.register_uri( - 'POST', settings.GOOGLE_ANALYTICS_TRACKING_URL, exc=ConnectionError) - track_event('category', 'action', 'label') + "POST", settings.GOOGLE_ANALYTICS_TRACKING_URL, exc=ConnectionError + ) + track_event("category", "action", "label") # unless they're not... settings.GOOGLE_ANALYTICS_TRACKING_RAISE_ERRORS = True with pytest.raises(ConnectionError): - track_event('category', 'action', 'label') + track_event("category", "action", "label") def test_bad_responses_swallowed(settings, mock_requests): - settings.GOOGLE_ANALYTICS_ACCOUNT = 'UA-XXXX-1' + settings.GOOGLE_ANALYTICS_ACCOUNT = "UA-XXXX-1" settings.GOOGLE_ANALYTICS_TRACKING_RAISE_ERRORS = False mock_requests.register_uri( - 'POST', settings.GOOGLE_ANALYTICS_TRACKING_URL, status_code=500) - track_event('category', 'action', 'label') + "POST", settings.GOOGLE_ANALYTICS_TRACKING_URL, status_code=500 + ) + track_event("category", "action", "label") # unless they're not... settings.GOOGLE_ANALYTICS_TRACKING_RAISE_ERRORS = True with pytest.raises(HTTPError): - track_event('category', 'action', 'label', raise_errors=True) + track_event("category", "action", "label", raise_errors=True) diff --git a/kuma/core/tests/test_helpers.py b/kuma/core/tests/test_helpers.py index 3c560d57637..841e9fc10d2 100644 --- a/kuma/core/tests/test_helpers.py +++ b/kuma/core/tests/test_helpers.py @@ -1,5 +1,3 @@ - - from datetime import datetime from unittest import mock @@ -15,175 +13,183 @@ from kuma.users.tests import UserTestCase from ..exceptions import DateTimeFormatError -from ..templatetags.jinja_helpers import (assert_function, datetimeformat, - get_soapbox_messages, in_utc, - jsonencode, page_title, - soapbox_messages, yesno) +from ..templatetags.jinja_helpers import ( + assert_function, + datetimeformat, + get_soapbox_messages, + in_utc, + jsonencode, + page_title, + soapbox_messages, + yesno, +) def test_assert_function(): with pytest.raises(RuntimeError) as exc: - assert_function(False, 'Message') - assert str(exc.value) == 'Failed assertion: Message' + assert_function(False, "Message") + assert str(exc.value) == "Failed assertion: Message" def test_assert_function_no_message(): with pytest.raises(RuntimeError) as exc: assert_function(False) - assert str(exc.value) == 'Failed assertion' + assert str(exc.value) == "Failed assertion" def test_assert_function_passes(): - assert assert_function(True, 'Message') == '' + assert assert_function(True, "Message") == "" class TestYesNo(KumaTestCase): - def test_yesno(self): - assert 'Yes' == yesno(True) - assert 'No' == yesno(False) - assert 'Yes' == yesno(1) - assert 'No' == yesno(0) + assert "Yes" == yesno(True) + assert "No" == yesno(False) + assert "Yes" == yesno(1) + assert "No" == yesno(0) class TestSoapbox(KumaTestCase): - def test_global_message(self): - m = Message(message='Global', is_global=True, is_active=True, url='/') + m = Message(message="Global", is_global=True, is_active=True, url="/") m.save() - assert m.message == get_soapbox_messages('/')[0].message - assert m.message == get_soapbox_messages('/en-US/')[0].message + assert m.message == get_soapbox_messages("/")[0].message + assert m.message == get_soapbox_messages("/en-US/")[0].message def test_subsection_message(self): - m = Message(message='Search down', is_global=False, is_active=True, - url='/search') + m = Message( + message="Search down", is_global=False, is_active=True, url="/search" + ) m.save() - assert 0 == len(get_soapbox_messages('/')) - assert 0 == len(get_soapbox_messages('/docs')) - assert 0 == len(get_soapbox_messages('/en-US/docs')) - assert m.message == get_soapbox_messages('/en-US/search')[0].message - assert m.message == get_soapbox_messages('/de/search')[0].message + assert 0 == len(get_soapbox_messages("/")) + assert 0 == len(get_soapbox_messages("/docs")) + assert 0 == len(get_soapbox_messages("/en-US/docs")) + assert m.message == get_soapbox_messages("/en-US/search")[0].message + assert m.message == get_soapbox_messages("/de/search")[0].message def test_message_with_url_is_link(self): - m = Message(message='Go to http://bit.ly/sample-demo', is_global=True, - is_active=True, url='/') + m = Message( + message="Go to http://bit.ly/sample-demo", + is_global=True, + is_active=True, + url="/", + ) m.save() - assert 'Go to ' \ - 'http://bit.ly/sample-demo' in soapbox_messages(get_soapbox_messages('/')) + assert ( + 'Go to ' + "http://bit.ly/sample-demo" + in soapbox_messages(get_soapbox_messages("/")) + ) class TestDateTimeFormat(UserTestCase): def setUp(self): super(TestDateTimeFormat, self).setUp() - url_ = reverse('home') - self.context = {'request': RequestFactory().get(url_)} - self.context['request'].LANGUAGE_CODE = 'en-US' - self.context['request'].user = self.user_model.objects.get(username='testuser01') + url_ = reverse("home") + self.context = {"request": RequestFactory().get(url_)} + self.context["request"].LANGUAGE_CODE = "en-US" + self.context["request"].user = self.user_model.objects.get( + username="testuser01" + ) def test_today(self): """Expects shortdatetime, format: Today at {time}.""" date_today = datetime.today() - value_expected = 'Today at %s' % format_time(date_today, - format='short', - locale='en_US') - value_returned = datetimeformat(self.context, date_today, - output='json') + value_expected = "Today at %s" % format_time( + date_today, format="short", locale="en_US" + ) + value_returned = datetimeformat(self.context, date_today, output="json") assert value_expected == value_returned def test_locale(self): """Expects shortdatetime in French.""" - self.context['request'].LANGUAGE_CODE = 'fr' + self.context["request"].LANGUAGE_CODE = "fr" value_test = datetime.fromordinal(733900) - value_expected = format_datetime(value_test, format='short', - locale='fr') - value_returned = datetimeformat(self.context, value_test, - output='json') + value_expected = format_datetime(value_test, format="short", locale="fr") + value_returned = datetimeformat(self.context, value_test, output="json") assert value_expected == value_returned def test_default(self): """Expects shortdatetime.""" value_test = datetime.fromordinal(733900) - value_expected = format_datetime(value_test, format='short', - locale='en_US') - value_returned = datetimeformat(self.context, value_test, - output='json') + value_expected = format_datetime(value_test, format="short", locale="en_US") + value_returned = datetimeformat(self.context, value_test, output="json") assert value_expected == value_returned def test_longdatetime(self): """Expects long format.""" value_test = datetime.fromordinal(733900) tzvalue = pytz.timezone(settings.TIME_ZONE).localize(value_test) - value_expected = format_datetime(tzvalue, format='long', - locale='en_US') - value_returned = datetimeformat(self.context, value_test, - format='longdatetime', - output='json') + value_expected = format_datetime(tzvalue, format="long", locale="en_US") + value_returned = datetimeformat( + self.context, value_test, format="longdatetime", output="json" + ) assert value_expected == value_returned def test_date(self): """Expects date format.""" value_test = datetime.fromordinal(733900) - value_expected = format_date(value_test, locale='en_US') - value_returned = datetimeformat(self.context, value_test, - format='date', - output='json') + value_expected = format_date(value_test, locale="en_US") + value_returned = datetimeformat( + self.context, value_test, format="date", output="json" + ) assert value_expected == value_returned def test_time(self): """Expects time format.""" value_test = datetime.fromordinal(733900) - value_expected = format_time(value_test, locale='en_US') - value_returned = datetimeformat(self.context, value_test, - format='time', - output='json') + value_expected = format_time(value_test, locale="en_US") + value_returned = datetimeformat( + self.context, value_test, format="time", output="json" + ) assert value_expected == value_returned def test_datetime(self): """Expects datetime format.""" value_test = datetime.fromordinal(733900) - value_expected = format_datetime(value_test, locale='en_US') - value_returned = datetimeformat(self.context, value_test, - format='datetime', - output='json') + value_expected = format_datetime(value_test, locale="en_US") + value_returned = datetimeformat( + self.context, value_test, format="datetime", output="json" + ) assert value_expected == value_returned def test_unknown_format(self): """Unknown format raises DateTimeFormatError.""" date_today = datetime.today() with pytest.raises(DateTimeFormatError): - datetimeformat(self.context, date_today, format='unknown') + datetimeformat(self.context, date_today, format="unknown") - @mock.patch('babel.dates.format_datetime') + @mock.patch("babel.dates.format_datetime") def test_broken_format(self, mocked_format_datetime): value_test = datetime.fromordinal(733900) - value_english = format_datetime(value_test, locale='en_US') - self.context['request'].LANGUAGE_CODE = 'fr' + value_english = format_datetime(value_test, locale="en_US") + self.context["request"].LANGUAGE_CODE = "fr" mocked_format_datetime.side_effect = [ # first call is returning a KeyError as if the format is broken KeyError, # second call returns the English fallback version as expected value_english, ] - value_returned = datetimeformat(self.context, value_test, - format='datetime', - output='json') + value_returned = datetimeformat( + self.context, value_test, format="datetime", output="json" + ) assert value_english == value_returned def test_invalid_value(self): """Passing invalid value raises ValueError.""" with pytest.raises(ValueError): - datetimeformat(self.context, 'invalid') + datetimeformat(self.context, "invalid") def test_json_helper(self): - assert 'false' == jsonencode(False) - assert '{"foo": "bar"}' == jsonencode({'foo': 'bar'}) + assert "false" == jsonencode(False) + assert '{"foo": "bar"}' == jsonencode({"foo": "bar"}) def test_user_timezone(self): """Shows time in user timezone.""" value_test = datetime.fromordinal(733900) # Choose user with non default timezone - user = self.user_model.objects.get(username='admin') - self.context['request'].user = user + user = self.user_model.objects.get(username="admin") + self.context["request"].user = user # Convert tzvalue to user timezone default_tz = pytz.timezone(settings.TIME_ZONE) @@ -191,16 +197,16 @@ def test_user_timezone(self): tzvalue = default_tz.localize(value_test) tzvalue = user_tz.normalize(tzvalue.astimezone(user_tz)) - value_expected = format_datetime(tzvalue, format='long', - locale='en_US') - value_returned = datetimeformat(self.context, value_test, - format='longdatetime', - output='json') + value_expected = format_datetime(tzvalue, format="long", locale="en_US") + value_returned = datetimeformat( + self.context, value_test, format="longdatetime", output="json" + ) assert value_expected == value_returned class TestInUtc(KumaTestCase): """Test the in_utc datetime filter.""" + def test_utc(self): """Assert a time in UTC remains in UTC.""" dt = datetime(2016, 3, 10, 16, 12, tzinfo=pytz.utc) @@ -211,11 +217,11 @@ def test_aware(self): """Assert a time in a different time zone is converted to UTC.""" hour = 10 dt = datetime(2016, 3, 10, hour, 14) - dt = pytz.timezone('US/Central').localize(dt) + dt = pytz.timezone("US/Central").localize(dt) out = in_utc(dt) assert out == datetime(2016, 3, 10, hour + 6, 14, tzinfo=pytz.utc) - @override_settings(TIME_ZONE='US/Pacific') + @override_settings(TIME_ZONE="US/Pacific") def test_naive(self): """Assert that naïve datetimes are first converted to system time.""" hour = 8 @@ -226,8 +232,8 @@ def test_naive(self): class TestPageTitle(TestCase): def test_ascii(self): - assert page_title('title') == 'title | MDN' + assert page_title("title") == "title | MDN" def test_xss(self): - pt = page_title('') - assert pt == '</title><Img src=x onerror=alert(1)> | MDN' + pt = page_title("") + assert pt == "</title><Img src=x onerror=alert(1)> | MDN" diff --git a/kuma/core/tests/test_jobs.py b/kuma/core/tests/test_jobs.py index 3ecd2600fc7..53b7911abbb 100644 --- a/kuma/core/tests/test_jobs.py +++ b/kuma/core/tests/test_jobs.py @@ -1,5 +1,3 @@ - - from unittest import mock from kuma.core.tests import KumaTestCase @@ -9,33 +7,31 @@ class KumaJobTests(KumaTestCase): - def test_key_changes_with_version(self): job = KumaJob() - test_key = job.key('test') + test_key = job.key("test") job.version = 2 - self.assertNotEqual(test_key, job.key('test')) + self.assertNotEqual(test_key, job.key("test")) class GenerationJobTest(KumaTestCase): - def test_generation_same(self): job1 = GenerationJob(generation_args=[1]) job2 = GenerationJob(generation_args=[1]) - key1 = job1.key('test') - key2 = job2.key('test') + key1 = job1.key("test") + key2 = job2.key("test") assert key1 == key2 def test_invalidate_generation(self): job1 = GenerationJob() job2 = GenerationJob() - key1_gen1 = job1.key('test') - key2_gen1 = job2.key('test') + key1_gen1 = job1.key("test") + key2_gen1 = job2.key("test") assert key1_gen1 == key2_gen1 job1.invalidate_generation() - key1_gen2 = job1.key('test') - key2_gen2 = job2.key('test') + key1_gen2 = job1.key("test") + key2_gen2 = job2.key("test") assert key1_gen1 != key1_gen2 assert key1_gen2 == key2_gen2 @@ -44,30 +40,33 @@ class GenerationKeyJobTest(KumaTestCase): """Test the GenerationKeyJob.""" def setUp(self): - self.job = GenerationKeyJob(lifetime=GenerationJob.generation_lifetime, - for_class='kuma.core.GenerationJob', - generation_args=['foo']) + self.job = GenerationKeyJob( + lifetime=GenerationJob.generation_lifetime, + for_class="kuma.core.GenerationJob", + generation_args=["foo"], + ) def test_key(self): - assert self.job.key() == 'kuma.core.GenerationJob:foo:generation' + assert self.job.key() == "kuma.core.GenerationJob:foo:generation" - @mock.patch('kuma.core.jobs.crypto.get_random_string') + @mock.patch("kuma.core.jobs.crypto.get_random_string") def test_fetch(self, mock_rando): - mock_rando.return_value = 'abc123' - assert self.job.fetch() == 'abc123' + mock_rando.return_value = "abc123" + assert self.job.fetch() == "abc123" - @mock.patch('cacheback.utils.celery_refresh_cache.apply_async') + @mock.patch("cacheback.utils.celery_refresh_cache.apply_async") def test_refresh(self, mock_async): self.job.async_refresh() refresh_kwargs = { - 'call_args': (), - 'call_kwargs': {}, - 'klass_str': 'kuma.core.jobs.GenerationKeyJob', - 'obj_args': (), - 'obj_kwargs': { - 'lifetime': self.job.lifetime, - 'for_class': self.job.for_class, - 'generation_args': self.job.generation_args}, + "call_args": (), + "call_kwargs": {}, + "klass_str": "kuma.core.jobs.GenerationKeyJob", + "obj_args": (), + "obj_kwargs": { + "lifetime": self.job.lifetime, + "for_class": self.job.for_class, + "generation_args": self.job.generation_args, + }, } mock_async.assert_called_once_with(kwargs=refresh_kwargs) @@ -77,12 +76,11 @@ class EncodingJob(KumaJob): class TestCacheKeyWithDifferentEncoding(KumaTestCase): - def setUp(self): self.job = EncodingJob() def test_unicode_and_bytestring_args(self): - self.assertEqual(self.job.key(b'eggs'), self.job.key('eggs')) + self.assertEqual(self.job.key(b"eggs"), self.job.key("eggs")) def test_unicode_and_bytestring_kwargs(self): - self.assertEqual(self.job.key(spam=b'eggs'), self.job.key(spam='eggs')) + self.assertEqual(self.job.key(spam=b"eggs"), self.job.key(spam="eggs")) diff --git a/kuma/core/tests/test_locale_middleware.py b/kuma/core/tests/test_locale_middleware.py index d747e3a9f42..8e948407f40 100644 --- a/kuma/core/tests/test_locale_middleware.py +++ b/kuma/core/tests/test_locale_middleware.py @@ -1,5 +1,3 @@ - - import pytest from django.conf import settings from django.urls import reverse @@ -9,120 +7,121 @@ # Simple Accept-Language headers, one term SIMPLE_ACCEPT_CASES = ( - ('', 'en-US'), # No preference gets default en-US - ('en', 'en-US'), # Default en is en-US - ('en-US', 'en-US'), # Exact match for default - ('en-us', 'en-US'), # Case-insensitive match for default - ('fr-FR', 'fr'), # Overly-specified locale gets default - ('fr-fr', 'fr'), # Overly-specified match is case-insensitive + ("", "en-US"), # No preference gets default en-US + ("en", "en-US"), # Default en is en-US + ("en-US", "en-US"), # Exact match for default + ("en-us", "en-US"), # Case-insensitive match for default + ("fr-FR", "fr"), # Overly-specified locale gets default + ("fr-fr", "fr"), # Overly-specified match is case-insensitive ) # Real-world Accept-Language headers include quality value weights WEIGHTED_ACCEPT_CASES = ( - ('en, fr;q=0.5', 'en-US'), # English without region gets en-US - ('en-GB, fr-FR;q=0.5', 'en-US'), # Any English gets en-US - ('en-US, en;q=0.5', 'en-US'), # Request for en-US gets en-US - ('fr, en-US;q=0.5', 'fr'), # Exact match of non-English language - ('fr-FR, de-DE;q=0.5', 'fr'), # Highest locale-specific match wins - ('fr-FR, de;q=0.5', 'fr'), # First generic match wins - ('pt, fr;q=0.5', 'pt-PT'), # Generic Portuguese matches pt-PT - ('pt-BR, en-US;q=0.5', 'pt-BR'), # Portuguese-Brazil matches - ('qaz-ZZ, fr-FR;q=0.5', 'fr'), # Respect partial match on prefix - ('qaz-ZZ, qaz;q=0.5', False), # No matches gets default en-US - ('zh-Hant, fr;q=0.5', 'zh-TW'), # Traditional Chinese matches zh-TW - ('*', 'en-US'), # Any-language case gets default + ("en, fr;q=0.5", "en-US"), # English without region gets en-US + ("en-GB, fr-FR;q=0.5", "en-US"), # Any English gets en-US + ("en-US, en;q=0.5", "en-US"), # Request for en-US gets en-US + ("fr, en-US;q=0.5", "fr"), # Exact match of non-English language + ("fr-FR, de-DE;q=0.5", "fr"), # Highest locale-specific match wins + ("fr-FR, de;q=0.5", "fr"), # First generic match wins + ("pt, fr;q=0.5", "pt-PT"), # Generic Portuguese matches pt-PT + ("pt-BR, en-US;q=0.5", "pt-BR"), # Portuguese-Brazil matches + ("qaz-ZZ, fr-FR;q=0.5", "fr"), # Respect partial match on prefix + ("qaz-ZZ, qaz;q=0.5", False), # No matches gets default en-US + ("zh-Hant, fr;q=0.5", "zh-TW"), # Traditional Chinese matches zh-TW + ("*", "en-US"), # Any-language case gets default ) -PICKER_CASES = SIMPLE_ACCEPT_CASES + WEIGHTED_ACCEPT_CASES + ( - ('xx', 'en-US'), # Unknown in Accept-Language gets default +PICKER_CASES = ( + SIMPLE_ACCEPT_CASES + + WEIGHTED_ACCEPT_CASES + + (("xx", "en-US"),) # Unknown in Accept-Language gets default ) REDIRECT_CASES = [ - ('cn', 'zh-CN'), # General to locale-specific in different general locale - ('pt', 'pt-PT'), # General to locale-specific - ('PT', 'pt-PT'), # It does a case-insensitive comparison - ('fr-FR', 'fr'), # Country-specific to language-only - ('Fr-fr', 'fr'), # It does a case-insensitive comparison - ('en', 'en-US'), # Ensure that en redirects to en-US, case insensitive - ('En', 'en-US'), - ('EN', 'en-US'), - ('zh-Hans', 'zh-CN'), # Django-preferred to Mozilla standard locale - ('zh_tw', 'zh-TW'), # Underscore and capitalization fix + ("cn", "zh-CN"), # General to locale-specific in different general locale + ("pt", "pt-PT"), # General to locale-specific + ("PT", "pt-PT"), # It does a case-insensitive comparison + ("fr-FR", "fr"), # Country-specific to language-only + ("Fr-fr", "fr"), # It does a case-insensitive comparison + ("en", "en-US"), # Ensure that en redirects to en-US, case insensitive + ("En", "en-US"), + ("EN", "en-US"), + ("zh-Hans", "zh-CN"), # Django-preferred to Mozilla standard locale + ("zh_tw", "zh-TW"), # Underscore and capitalization fix ] + [(orig, new) for (orig, new) in SIMPLE_ACCEPT_CASES if orig != new] def test_locale_middleware_redirect_when_not_homepage(client, db): - '''The LocaleMiddleware redirects via 302 when it's not the homepage.''' - response = client.get('/docs/Web/HTML') + """The LocaleMiddleware redirects via 302 when it's not the homepage.""" + response = client.get("/docs/Web/HTML") assert response.status_code == 302 - assert response['Location'] == '/en-US/docs/Web/HTML' + assert response["Location"] == "/en-US/docs/Web/HTML" assert_shared_cache_header(response) -@pytest.mark.parametrize('accept_language,locale', PICKER_CASES) +@pytest.mark.parametrize("accept_language,locale", PICKER_CASES) def test_locale_middleware_picker(accept_language, locale, client, db): - '''The LocaleMiddleware picks locale from the Accept-Language header.''' - response = client.get('/', HTTP_ACCEPT_LANGUAGE=accept_language) + """The LocaleMiddleware picks locale from the Accept-Language header.""" + response = client.get("/", HTTP_ACCEPT_LANGUAGE=accept_language) assert response.status_code == 301 - url_locale = locale or 'en-US' - assert response['Location'] == ('/%s/' % url_locale) + url_locale = locale or "en-US" + assert response["Location"] == ("/%s/" % url_locale) assert_shared_cache_header(response) -@pytest.mark.parametrize('original,fixed', REDIRECT_CASES) +@pytest.mark.parametrize("original,fixed", REDIRECT_CASES) def test_locale_middleware_fixer(original, fixed, client, db): - '''The LocaleStandardizerMiddleware redirects non-standard locale URLs.''' - response = client.get(('/%s/' % original) if original else '/') - if original == '': + """The LocaleStandardizerMiddleware redirects non-standard locale URLs.""" + response = client.get(("/%s/" % original) if original else "/") + if original == "": # LocaleMiddleware handles this case, and it's a 301 instead # of a 302 since it's the homepage. expected_status_code = 301 else: expected_status_code = 302 assert response.status_code == expected_status_code - assert response['Location'] == '/%s/' % fixed + assert response["Location"] == "/%s/" % fixed assert_shared_cache_header(response) def test_locale_middleware_fixer_confusion(client, db): - '''The LocaleStandardizerMiddleware treats unknown locales as 404s.''' - response = client.get('/xx/') + """The LocaleStandardizerMiddleware treats unknown locales as 404s.""" + response = client.get("/xx/") assert response.status_code == 404 def test_locale_middleware_language_cookie(client, db): - '''The LocaleMiddleware uses the language cookie over the header.''' - client.cookies.load({settings.LANGUAGE_COOKIE_NAME: 'bn'}) - response = client.get('/', HTTP_ACCEPT_LANGUAGE='fr') + """The LocaleMiddleware uses the language cookie over the header.""" + client.cookies.load({settings.LANGUAGE_COOKIE_NAME: "bn"}) + response = client.get("/", HTTP_ACCEPT_LANGUAGE="fr") assert response.status_code == 301 - assert response['Location'] == '/bn/' + assert response["Location"] == "/bn/" assert_shared_cache_header(response) -@pytest.mark.parametrize('path', ('/', '/en-US/')) +@pytest.mark.parametrize("path", ("/", "/en-US/")) def test_lang_selector_middleware(path, client): - '''The LangSelectorMiddleware redirects on the ?lang query first.''' - client.cookies.load({settings.LANGUAGE_COOKIE_NAME: 'bn'}) - response = client.get(f'{path}?lang=fr', - HTTP_ACCEPT_LANGUAGE='en;q=0.9, fr;q=0.8') + """The LangSelectorMiddleware redirects on the ?lang query first.""" + client.cookies.load({settings.LANGUAGE_COOKIE_NAME: "bn"}) + response = client.get(f"{path}?lang=fr", HTTP_ACCEPT_LANGUAGE="en;q=0.9, fr;q=0.8") assert response.status_code == 302 - assert response['Location'] == '/fr/' + assert response["Location"] == "/fr/" assert_shared_cache_header(response) def test_lang_selector_middleware_preserves_query(root_doc, client): - '''The LangSelectorMiddleware preserves other parameters.''' - url = reverse('wiki.json') - query = {'lang': root_doc.locale, 'slug': root_doc.slug} + """The LangSelectorMiddleware preserves other parameters.""" + url = reverse("wiki.json") + query = {"lang": root_doc.locale, "slug": root_doc.slug} response = client.get(url, query) assert response.status_code == 302 - expected = f'{url}?slug={root_doc.slug}' - assert response['Location'] == expected + expected = f"{url}?slug={root_doc.slug}" + assert response["Location"] == expected assert_shared_cache_header(response) def test_lang_selector_middleware_no_change(client, db): - '''The LangSelectorMiddleware redirects on the same ?lang query.''' - response = client.get('/fr/?lang=fr') + """The LangSelectorMiddleware redirects on the same ?lang query.""" + response = client.get("/fr/?lang=fr") assert response.status_code == 302 - assert response['Location'] == '/fr/' + assert response["Location"] == "/fr/" assert_shared_cache_header(response) @@ -130,19 +129,19 @@ def test_lang_selector_middleware_no_change(client, db): # chance with a locale prefix. # Subset of tests.headless.map_301.LEGACY_URLS LEGACY_404S = ( - '/index.php', - '/index.php?title=En/HTML/Canvas&revision=110', - '/patches', - '/patches/foo', - '/web-tech', - '/web-tech/feed/atom/', - '/css/wiki.css', - '/css/base.css', + "/index.php", + "/index.php?title=En/HTML/Canvas&revision=110", + "/patches", + "/patches/foo", + "/web-tech", + "/web-tech/feed/atom/", + "/css/wiki.css", + "/css/base.css", ) -@pytest.mark.parametrize('path', LEGACY_404S) +@pytest.mark.parametrize("path", LEGACY_404S) def test_locale_middleware_legacy_404s(client, path, db): - '''Legacy paths should be 404s, not get a locale prefix.''' + """Legacy paths should be 404s, not get a locale prefix.""" response = client.get(path) assert response.status_code == 404 diff --git a/kuma/core/tests/test_middleware.py b/kuma/core/tests/test_middleware.py index 0ea8008239c..885d3ca194a 100644 --- a/kuma/core/tests/test_middleware.py +++ b/kuma/core/tests/test_middleware.py @@ -13,52 +13,51 @@ ) -@pytest.mark.parametrize('path', ('/missing_url', '/missing_url/')) +@pytest.mark.parametrize("path", ("/missing_url", "/missing_url/")) def test_slash_middleware_keep_404(client, db, path): - '''The SlashMiddleware retains 404s.''' + """The SlashMiddleware retains 404s.""" response = client.get(path) assert response.status_code == 404 def test_slash_middleware_removes_slash(client, db): - '''The SlashMiddleware fixes a URL that shouldn't have a trailing slash.''' - response = client.get('/contribute.json/') + """The SlashMiddleware fixes a URL that shouldn't have a trailing slash.""" + response = client.get("/contribute.json/") assert response.status_code == 301 - assert response['Location'].endswith('/contribute.json') + assert response["Location"].endswith("/contribute.json") -@pytest.mark.parametrize('path', ('/admin', '/en-US')) +@pytest.mark.parametrize("path", ("/admin", "/en-US")) def test_slash_middleware_adds_slash(path, client, db): - '''The SlashMiddleware fixes a URL that should have a trailing slash.''' + """The SlashMiddleware fixes a URL that should have a trailing slash.""" response = client.get(path) assert response.status_code == 301 - assert response['Location'].endswith(path + '/') + assert response["Location"].endswith(path + "/") def test_slash_middleware_retains_querystring(client, db): - '''The SlashMiddleware handles encoded querystrings.''' - response = client.get('/contribute.json/?xxx=%C3%83') + """The SlashMiddleware handles encoded querystrings.""" + response = client.get("/contribute.json/?xxx=%C3%83") assert response.status_code == 301 - assert response['Location'].endswith('/contribute.json?xxx=%C3%83') + assert response["Location"].endswith("/contribute.json?xxx=%C3%83") @pytest.mark.parametrize( - 'forwarded_for,remote_addr', - (('1.1.1.1', '1.1.1.1'), - ('2.2.2.2', '2.2.2.2'), - ('3.3.3.3, 4.4.4.4', '3.3.3.3'))) + "forwarded_for,remote_addr", + (("1.1.1.1", "1.1.1.1"), ("2.2.2.2", "2.2.2.2"), ("3.3.3.3, 4.4.4.4", "3.3.3.3")), +) def test_set_remote_addr_from_forwarded_for(rf, forwarded_for, remote_addr): - '''SetRemoteAddrFromForwardedFor parses the X-Forwarded-For Header.''' + """SetRemoteAddrFromForwardedFor parses the X-Forwarded-For Header.""" rf = RequestFactory() middleware = SetRemoteAddrFromForwardedFor(lambda req: None) - request = rf.get('/', HTTP_X_FORWARDED_FOR=forwarded_for) + request = rf.get("/", HTTP_X_FORWARDED_FOR=forwarded_for) middleware(request) - assert request.META['REMOTE_ADDR'] == remote_addr + assert request.META["REMOTE_ADDR"] == remote_addr def test_force_anonymous_session_middleware(rf, settings): - request = rf.get('/foo') - request.COOKIES[settings.SESSION_COOKIE_NAME] = 'totallyfake' + request = rf.get("/foo") + request.COOKIES[settings.SESSION_COOKIE_NAME] = "totallyfake" mock_response = MagicMock() middleware = ForceAnonymousSessionMiddleware(lambda req: mock_response) @@ -70,23 +69,25 @@ def test_force_anonymous_session_middleware(rf, settings): @pytest.mark.parametrize( - 'host,key,expected', - (('wiki', 'WIKI_HOST', None), - ('demos', 'ATTACHMENT_HOST', 'kuma.urls_untrusted'), - ('demos-origin', 'ATTACHMENT_ORIGIN', 'kuma.urls_untrusted')), - ids=('wiki', 'attachment', 'attachment-origin') + "host,key,expected", + ( + ("wiki", "WIKI_HOST", None), + ("demos", "ATTACHMENT_HOST", "kuma.urls_untrusted"), + ("demos-origin", "ATTACHMENT_ORIGIN", "kuma.urls_untrusted"), + ), + ids=("wiki", "attachment", "attachment-origin"), ) def test_restricted_endpoints_middleware(rf, settings, host, key, expected): setattr(settings, key, host) settings.ENABLE_RESTRICTIONS_BY_HOST = True settings.ALLOWED_HOSTS.append(host) middleware = RestrictedEndpointsMiddleware(lambda req: None) - request = rf.get('/foo', HTTP_HOST=host) + request = rf.get("/foo", HTTP_HOST=host) middleware(request) if expected: assert request.urlconf == expected else: - assert not hasattr(request, 'urlconf') + assert not hasattr(request, "urlconf") def test_restricted_endpoints_middleware_when_disabled(settings): @@ -96,19 +97,19 @@ def test_restricted_endpoints_middleware_when_disabled(settings): def test_waffle_cookie_domain_middleware(rf, settings): - settings.WAFFLE_COOKIE = 'dwf_%s' - settings.WAFFLE_COOKIE_DOMAIN = 'mdn.dev' + settings.WAFFLE_COOKIE = "dwf_%s" + settings.WAFFLE_COOKIE_DOMAIN = "mdn.dev" resp = HttpResponse() - resp.set_cookie('some_key', 'some_value', domain=None) - resp.set_cookie('another_key', 'another_value', domain='another.domain') + resp.set_cookie("some_key", "some_value", domain=None) + resp.set_cookie("another_key", "another_value", domain="another.domain") middleware = WaffleWithCookieDomainMiddleware(lambda req: resp) - request = rf.get('/foo') + request = rf.get("/foo") request.waffles = { - 'contrib_beta': (True, False), - 'developer_needs': (True, False), + "contrib_beta": (True, False), + "developer_needs": (True, False), } response = middleware(request) - assert response.cookies['some_key']['domain'] == '' - assert response.cookies['another_key']['domain'] == 'another.domain' - assert response.cookies['dwf_contrib_beta']['domain'] == 'mdn.dev' - assert response.cookies['dwf_developer_needs']['domain'] == 'mdn.dev' + assert response.cookies["some_key"]["domain"] == "" + assert response.cookies["another_key"]["domain"] == "another.domain" + assert response.cookies["dwf_contrib_beta"]["domain"] == "mdn.dev" + assert response.cookies["dwf_developer_needs"]["domain"] == "mdn.dev" diff --git a/kuma/core/tests/test_misc.py b/kuma/core/tests/test_misc.py index 1251c4d606c..c6d9182a566 100644 --- a/kuma/core/tests/test_misc.py +++ b/kuma/core/tests/test_misc.py @@ -1,5 +1,3 @@ - - from django.test import RequestFactory from . import KumaTestCase @@ -11,14 +9,15 @@ class TestNextUrl(KumaTestCase): Tests that the next_url value is properly set, including query string """ + rf = RequestFactory() def test_basic(self): - path = '/one/two' + path = "/one/two" request = self.rf.get(path) - assert path == next_url(request)['next_url'] + assert path == next_url(request)["next_url"] def test_querystring(self): - path = '/one/two?something' + path = "/one/two?something" request = self.rf.get(path) - assert path == next_url(request)['next_url'] + assert path == next_url(request)["next_url"] diff --git a/kuma/core/tests/test_models.py b/kuma/core/tests/test_models.py index 14c88585e5e..c4c909cd071 100644 --- a/kuma/core/tests/test_models.py +++ b/kuma/core/tests/test_models.py @@ -1,5 +1,3 @@ - - from datetime import date, timedelta from django.test import TestCase @@ -10,27 +8,27 @@ class RevisionIPTests(TestCase): def test_delete_older_than_default_30_days(self): old_date = date.today() - timedelta(days=31) - IPBan(ip='127.0.0.1', created=old_date).save() + IPBan(ip="127.0.0.1", created=old_date).save() assert 1 == IPBan.objects.count() IPBan.objects.delete_old() assert 0 == IPBan.objects.count() def test_delete_older_than_days_argument(self): ban_date = date.today() - timedelta(days=5) - IPBan(ip='127.0.0.1', created=ban_date).save() + IPBan(ip="127.0.0.1", created=ban_date).save() assert 1 == IPBan.objects.count() IPBan.objects.delete_old(days=4) assert 0 == IPBan.objects.count() def test_delete_older_than_only_deletes_older_than(self): oldest_date = date.today() - timedelta(days=31) - IPBan(ip='127.0.0.1', created=oldest_date).save() + IPBan(ip="127.0.0.1", created=oldest_date).save() old_date = date.today() - timedelta(days=29) - IPBan(ip='127.0.0.2', created=old_date).save() + IPBan(ip="127.0.0.2", created=old_date).save() now_date = date.today() - IPBan(ip='127.0.0.3', created=now_date).save() + IPBan(ip="127.0.0.3", created=now_date).save() assert 3 == IPBan.objects.count() IPBan.objects.delete_old() assert 2 == IPBan.objects.count() diff --git a/kuma/core/tests/test_pagination.py b/kuma/core/tests/test_pagination.py index 2d218cc7938..3ee3765ee3e 100644 --- a/kuma/core/tests/test_pagination.py +++ b/kuma/core/tests/test_pagination.py @@ -1,5 +1,3 @@ - - import pyquery from django.test import RequestFactory @@ -10,49 +8,46 @@ def test_paginated_url(): """Avoid duplicating page param in pagination.""" - url = urlparams(reverse('search'), q='bookmarks', page=2) + url = urlparams(reverse("search"), q="bookmarks", page=2) request = RequestFactory().get(url) queryset = [{}, {}] paginated = paginate(request, queryset) - assert (paginated.url == - request.build_absolute_uri(request.path) + '?q=bookmarks') + assert paginated.url == request.build_absolute_uri(request.path) + "?q=bookmarks" def test_invalid_page_param(): - url = urlparams(reverse('search'), page='a') + url = urlparams(reverse("search"), page="a") request = RequestFactory().get(url) queryset = range(100) paginated = paginate(request, queryset) - assert (paginated.url == - request.build_absolute_uri(request.path) + '?') + assert paginated.url == request.build_absolute_uri(request.path) + "?" def test_paginator_filter_num_elements_start(): # Correct number of
  • s on page 1. - url = reverse('search') + url = reverse("search") request = RequestFactory().get(url) pager = paginate(request, range(100), per_page=9) html = paginator(pager) doc = pyquery.PyQuery(html) - assert 11 == len(doc('li')) + assert 11 == len(doc("li")) def test_paginator_filter_num_elements_middle(): # Correct number of
  • s in the middle. - url = urlparams(reverse('search'), page=10) + url = urlparams(reverse("search"), page=10) request = RequestFactory().get(url) pager = paginate(request, range(200), per_page=10) html = paginator(pager) doc = pyquery.PyQuery(html) - assert 13 == len(doc('li')) + assert 13 == len(doc("li")) def test_paginator_filter_current_selected(): # Ensure the current page has 'class="selected"'. - url = urlparams(reverse('search'), page=10) + url = urlparams(reverse("search"), page=10) request = RequestFactory().get(url) pager = paginate(request, range(200), per_page=10) html = paginator(pager) doc = pyquery.PyQuery(html) - assert (doc('li.selected a').attr('href') == - 'http://testserver/en-US/search?page=10') + assert doc("li.selected a").attr("href") == "http://testserver/en-US/search?page=10" diff --git a/kuma/core/tests/test_settings.py b/kuma/core/tests/test_settings.py index c5dd78a03cf..dd18ee96c32 100644 --- a/kuma/core/tests/test_settings.py +++ b/kuma/core/tests/test_settings.py @@ -1,4 +1,4 @@ -'''Check that settings are consistent.''' +"""Check that settings are consistent.""" import pytest @@ -11,14 +11,11 @@ def test_accepted_locales(): assert settings.ACCEPTED_LOCALES[0] == settings.LANGUAGE_CODE -@pytest.mark.parametrize( - 'primary,secondary', - (('pt-PT', 'pt-BR'), - ('zh-CN', 'zh-TW'), - )) +@pytest.mark.parametrize("primary,secondary", (("pt-PT", "pt-BR"), ("zh-CN", "zh-TW"),)) def test_preferred_locale_codes(primary, secondary): - assert (settings.ACCEPTED_LOCALES.index(primary) < - settings.ACCEPTED_LOCALES.index(secondary)) + assert settings.ACCEPTED_LOCALES.index(primary) < settings.ACCEPTED_LOCALES.index( + secondary + ) @pytest.mark.parametrize("locale", settings.RTL_LANGUAGES) diff --git a/kuma/core/tests/test_taggit_extras.py b/kuma/core/tests/test_taggit_extras.py index 5238635da80..60942eb16eb 100644 --- a/kuma/core/tests/test_taggit_extras.py +++ b/kuma/core/tests/test_taggit_extras.py @@ -1,5 +1,3 @@ - - import pytest from django.test import TestCase from taggit.models import Tag @@ -20,11 +18,11 @@ def test_all_ns(self): apple = self.food_model.objects.create(name="apple") expected_tags = { - '': ['foo', 'bar', 'baz'], - 'int:': ['int:1', 'int:2'], - 'string:': ['string:asdf'], - 'color:': ['color:red'], - 'system:contest:': ['system:contest:finalist'] + "": ["foo", "bar", "baz"], + "int:": ["int:1", "int:2"], + "string:": ["string:asdf"], + "color:": ["color:red"], + "system:contest:": ["system:contest:finalist"], } for tags in expected_tags.values(): @@ -43,25 +41,25 @@ def test_all_ns(self): def test_clear_ns(self): """Tags can be selectively cleared by namespace""" apple = self.food_model.objects.create(name="apple") - tags_cleared = ['a:1', 'a:2', 'a:3'] - tags_not_cleared = ['1', '2', 'b:1', 'b:2', 'c:1'] + tags_cleared = ["a:1", "a:2", "a:3"] + tags_not_cleared = ["1", "2", "b:1", "b:2", "c:1"] apple.tags.add(*tags_cleared) apple.tags.add(*tags_not_cleared) - apple.tags.clear_ns('a:') + apple.tags.clear_ns("a:") self.assert_tags_equal(apple.tags.all(), tags_not_cleared) def test_set_ns(self): """Tags can be selectively set by namespace""" apple = self.food_model.objects.create(name="apple") - tags_before_set = ['a:1', 'a:2', 'a:3'] - tags_after_set = ['a:4', 'a:5', 'a:6'] - tags_not_set = ['1', '2', 'b:1', 'b:2', 'c:1'] + tags_before_set = ["a:1", "a:2", "a:3"] + tags_after_set = ["a:4", "a:5", "a:6"] + tags_not_set = ["1", "2", "b:1", "b:2", "c:1"] apple.tags.add(*tags_before_set) apple.tags.add(*tags_not_set) - apple.tags.set_ns('a:', *tags_after_set) + apple.tags.set_ns("a:", *tags_after_set) self.assert_tags_equal(apple.tags.all(), tags_after_set + tags_not_set) @@ -70,23 +68,23 @@ def test_add_ns(self): namespace tacked on.""" apple = self.food_model.objects.create(name="apple") - tags = ['foo', 'bar', 'baz'] - apple.tags.add_ns('a:', *tags) + tags = ["foo", "bar", "baz"] + apple.tags.add_ns("a:", *tags) - self.assert_tags_equal(apple.tags.all(), ['a:%s' % t for t in tags]) + self.assert_tags_equal(apple.tags.all(), ["a:%s" % t for t in tags]) def test_duplicate_names_to_create(self): apple = self.food_model.objects.create(name="apple") - tags = ['tasty', 'Tasty'] - apple.tags.add_ns('a:', *tags) + tags = ["tasty", "Tasty"] + apple.tags.add_ns("a:", *tags) assert apple.tags.count() == 1 tag = apple.tags.get() - assert tag.name in ('a:tasty', 'a:Tasty') + assert tag.name in ("a:tasty", "a:Tasty") def test_duplicate_names_existing(self): apple = self.food_model.objects.create(name="apple") - Tag.objects.create(name='a:Red') - Tag.objects.create(name='a:Tasty') - tags = ['tasty', 'Tasty', 'Red', 'red'] - apple.tags.add_ns('a:', *tags) - self.assert_tags_equal(apple.tags.all(), ['a:Tasty', 'a:Red']) + Tag.objects.create(name="a:Red") + Tag.objects.create(name="a:Tasty") + tags = ["tasty", "Tasty", "Red", "red"] + apple.tags.add_ns("a:", *tags) + self.assert_tags_equal(apple.tags.all(), ["a:Tasty", "a:Red"]) diff --git a/kuma/core/tests/test_templates.py b/kuma/core/tests/test_templates.py index 4229654c4b6..2057ab9e891 100644 --- a/kuma/core/tests/test_templates.py +++ b/kuma/core/tests/test_templates.py @@ -1,5 +1,3 @@ - - import os from django.conf import settings @@ -15,14 +13,15 @@ class MockRequestTests(KumaTestCase): """Base class for tests that need a mock request""" + rf = RequestFactory() def setUp(self): super(MockRequestTests, self).setUp() self.user = AnonymousUser() - self.request = self.rf.get('/') + self.request = self.rf.get("/") self.request.user = self.user - self.request.LANGUAGE_CODE = 'en-US' + self.request.LANGUAGE_CODE = "en-US" class BaseTemplateTests(MockRequestTests): @@ -30,20 +29,20 @@ class BaseTemplateTests(MockRequestTests): def setUp(self): super(BaseTemplateTests, self).setUp() - self.template = 'base.html' + self.template = "base.html" def test_no_dir_attribute(self): html = render_to_string(self.template, request=self.request) doc = pq(html) - dir_attr = doc('html').attr['dir'] - assert 'ltr' == dir_attr + dir_attr = doc("html").attr["dir"] + assert "ltr" == dir_attr def test_rtl_dir_attribute(self): - translation.activate('ar') + translation.activate("ar") html = render_to_string(self.template, request=self.request) doc = pq(html) - dir_attr = doc('html').attr['dir'] - assert 'rtl' == dir_attr + dir_attr = doc("html").attr["dir"] + assert "rtl" == dir_attr def test_lang_switcher(self): translation.activate("bn") @@ -60,33 +59,36 @@ class ErrorListTests(MockRequestTests): def setUp(self): super(ErrorListTests, self).setUp() params = { - 'DIRS': [os.path.join(settings.ROOT, 'jinja2')], - 'APP_DIRS': True, - 'NAME': 'jinja2', - 'OPTIONS': {}, + "DIRS": [os.path.join(settings.ROOT, "jinja2")], + "APP_DIRS": True, + "NAME": "jinja2", + "OPTIONS": {}, } self.engine = Jinja2(params) def test_escaping(self): """Make sure we escape HTML entities, lest we court XSS errors.""" + class MockForm(object): errors = True - auto_id = 'id_' + auto_id = "id_" def __iter__(self): return iter(self.visible_fields()) def visible_fields(self): - return [{'errors': ['<"evil&ness-field">']}] + return [{"errors": ['<"evil&ness-field">']}] def non_field_errors(self): return ['<"evil&ness-non-field">'] - source = ("""{% from "includes/error_list.html" import errorlist %}""" - """{{ errorlist(form) }}""") - context = {'form': MockForm()} + source = ( + """{% from "includes/error_list.html" import errorlist %}""" + """{{ errorlist(form) }}""" + ) + context = {"form": MockForm()} html = self.engine.from_string(source).render(context) assert '<"evil&ness' not in html - assert '<"evil&ness-field">' in html - assert '<"evil&ness-non-field">' in html + assert "<"evil&ness-field">" in html + assert "<"evil&ness-non-field">" in html diff --git a/kuma/core/tests/test_utils.py b/kuma/core/tests/test_utils.py index 62d83eb4acc..e9a02204b42 100644 --- a/kuma/core/tests/test_utils.py +++ b/kuma/core/tests/test_utils.py @@ -1,26 +1,28 @@ - - import pytest from django.utils.encoding import force_bytes from requests.exceptions import ConnectionError from kuma.core.utils import ( - order_params, requests_retry_session, safer_pyquery, smart_int) + order_params, + requests_retry_session, + safer_pyquery, + smart_int, +) def test_smart_int(): # Sanity check - assert 10 == smart_int('10') - assert 10 == smart_int('10.5') + assert 10 == smart_int("10") + assert 10 == smart_int("10.5") # Test int assert 10 == smart_int(10) # Invalid string - assert 0 == smart_int('invalid') + assert 0 == smart_int("invalid") # Empty string - assert 0 == smart_int('') + assert 0 == smart_int("") # Wrong type assert 0 == smart_int(None) @@ -28,11 +30,13 @@ def test_smart_int(): @pytest.mark.parametrize( - 'original,expected', - (('https://example.com', 'https://example.com'), - ('http://example.com?foo=bar&foo=', 'http://example.com?foo=&foo=bar'), - ('http://example.com?foo=bar&bar=baz', 'http://example.com?bar=baz&foo=bar'), - )) + "original,expected", + ( + ("https://example.com", "https://example.com"), + ("http://example.com?foo=bar&foo=", "http://example.com?foo=&foo=bar"), + ("http://example.com?foo=bar&bar=baz", "http://example.com?bar=baz&foo=bar"), + ), +) def test_order_params(original, expected): assert order_params(original) == expected @@ -43,50 +47,54 @@ def test_safer_pyquery(mock_requests): # My not setting up expectations, and if it got used, # these tests would raise a `NoMockAddress` exception. - parsed = safer_pyquery('https://www.peterbe.com') - assert parsed.outer_html() == '

    https://www.peterbe.com

    ' + parsed = safer_pyquery("https://www.peterbe.com") + assert parsed.outer_html() == "

    https://www.peterbe.com

    " # Note! Since this file uses `__future__.unicode_literals` the only # way to produce a byte string is to use force_bytes. # Byte strings in should continue to work. - parsed = safer_pyquery(force_bytes('https://www.peterbe.com')) - assert parsed.outer_html() == '

    https://www.peterbe.com

    ' + parsed = safer_pyquery(force_bytes("https://www.peterbe.com")) + assert parsed.outer_html() == "

    https://www.peterbe.com

    " # Non-ascii as Unicode - parsed = safer_pyquery('https://www.peterbe.com/ë') + parsed = safer_pyquery("https://www.peterbe.com/ë") - parsed = safer_pyquery(""" + parsed = safer_pyquery( + """ Bold! - """) - assert parsed('b').text() == 'Bold!' - parsed = safer_pyquery(""" + """ + ) + assert parsed("b").text() == "Bold!" + parsed = safer_pyquery( + """ URL - """) - assert parsed('a[href]').text() == 'URL' + """ + ) + assert parsed("a[href]").text() == "URL" def test_requests_retry_session(mock_requests): def absolute_url(uri): - return 'http://example.com' + uri + return "http://example.com" + uri - mock_requests.get(absolute_url('/a/ok'), text='hi') - mock_requests.get(absolute_url('/oh/noes'), text='bad!', status_code=504) - mock_requests.get(absolute_url('/oh/crap'), exc=ConnectionError) + mock_requests.get(absolute_url("/a/ok"), text="hi") + mock_requests.get(absolute_url("/oh/noes"), text="bad!", status_code=504) + mock_requests.get(absolute_url("/oh/crap"), exc=ConnectionError) session = requests_retry_session(status_forcelist=(504,)) - response_ok = session.get(absolute_url('/a/ok')) + response_ok = session.get(absolute_url("/a/ok")) assert response_ok.status_code == 200 - response_bad = session.get(absolute_url('/oh/noes')) + response_bad = session.get(absolute_url("/oh/noes")) assert response_bad.status_code == 504 with pytest.raises(ConnectionError): - session.get(absolute_url('/oh/crap')) + session.get(absolute_url("/oh/crap")) diff --git a/kuma/core/tests/test_validators.py b/kuma/core/tests/test_validators.py index 24dee0439c0..c9d11106573 100644 --- a/kuma/core/tests/test_validators.py +++ b/kuma/core/tests/test_validators.py @@ -1,43 +1,39 @@ - - from django.test import TestCase -from ..validators import (valid_javascript_identifier, - valid_jsonp_callback_value) +from ..validators import valid_javascript_identifier, valid_jsonp_callback_value class ValidatorTest(TestCase): - def test_valid_javascript_identifier(self): """ The function ``valid_javascript_identifier`` validates a given identifier according to the latest draft of the ECMAScript 5 Specification """ - self.assertTrue(valid_javascript_identifier(b'hello')) + self.assertTrue(valid_javascript_identifier(b"hello")) - self.assertFalse(valid_javascript_identifier(b'alert()')) + self.assertFalse(valid_javascript_identifier(b"alert()")) - self.assertFalse(valid_javascript_identifier(b'a-b')) + self.assertFalse(valid_javascript_identifier(b"a-b")) - self.assertFalse(valid_javascript_identifier(b'23foo')) + self.assertFalse(valid_javascript_identifier(b"23foo")) - self.assertTrue(valid_javascript_identifier(b'foo23')) + self.assertTrue(valid_javascript_identifier(b"foo23")) - self.assertTrue(valid_javascript_identifier(b'$210')) + self.assertTrue(valid_javascript_identifier(b"$210")) - self.assertTrue(valid_javascript_identifier('Stra\u00dfe')) + self.assertTrue(valid_javascript_identifier("Stra\u00dfe")) - self.assertTrue(valid_javascript_identifier(br'\u0062')) # 'b' + self.assertTrue(valid_javascript_identifier(br"\u0062")) # 'b' - self.assertFalse(valid_javascript_identifier(br'\u62')) + self.assertFalse(valid_javascript_identifier(br"\u62")) - self.assertFalse(valid_javascript_identifier(br'\u0020')) + self.assertFalse(valid_javascript_identifier(br"\u0020")) - self.assertTrue(valid_javascript_identifier(b'_bar')) + self.assertTrue(valid_javascript_identifier(b"_bar")) - self.assertTrue(valid_javascript_identifier(b'some_var')) + self.assertTrue(valid_javascript_identifier(b"some_var")) - self.assertTrue(valid_javascript_identifier(b'$')) + self.assertTrue(valid_javascript_identifier(b"$")) def test_valid_jsonp_callback_value(self): """ @@ -45,28 +41,28 @@ def test_valid_jsonp_callback_value(self): validating JSON-P callback parameter values: """ - self.assertTrue(valid_jsonp_callback_value('somevar')) + self.assertTrue(valid_jsonp_callback_value("somevar")) - self.assertFalse(valid_jsonp_callback_value('function')) + self.assertFalse(valid_jsonp_callback_value("function")) - self.assertFalse(valid_jsonp_callback_value(' somevar')) + self.assertFalse(valid_jsonp_callback_value(" somevar")) # It supports the possibility of '.' being present in the callback name, e.g. - self.assertTrue(valid_jsonp_callback_value('$.ajaxHandler')) + self.assertTrue(valid_jsonp_callback_value("$.ajaxHandler")) - self.assertFalse(valid_jsonp_callback_value('$.23')) + self.assertFalse(valid_jsonp_callback_value("$.23")) # As well as the pattern of providing an array index lookup, e.g. - self.assertTrue(valid_jsonp_callback_value('array_of_functions[42]')) + self.assertTrue(valid_jsonp_callback_value("array_of_functions[42]")) - self.assertTrue(valid_jsonp_callback_value('array_of_functions[42][1]')) + self.assertTrue(valid_jsonp_callback_value("array_of_functions[42][1]")) - self.assertTrue(valid_jsonp_callback_value('$.ajaxHandler[42][1].foo')) + self.assertTrue(valid_jsonp_callback_value("$.ajaxHandler[42][1].foo")) - self.assertFalse(valid_jsonp_callback_value('array_of_functions[42]foo[1]')) + self.assertFalse(valid_jsonp_callback_value("array_of_functions[42]foo[1]")) - self.assertFalse(valid_jsonp_callback_value('array_of_functions[]')) + self.assertFalse(valid_jsonp_callback_value("array_of_functions[]")) self.assertFalse(valid_jsonp_callback_value('array_of_functions["key"]')) diff --git a/kuma/core/tests/test_views.py b/kuma/core/tests/test_views.py index 8ae8f88af13..7e0d207b6be 100644 --- a/kuma/core/tests/test_views.py +++ b/kuma/core/tests/test_views.py @@ -1,5 +1,3 @@ - - import logging from unittest import mock @@ -13,50 +11,54 @@ from soapbox.models import Message from waffle.models import Flag -from . import (assert_no_cache_header, assert_shared_cache_header, - KumaTestCase) +from . import assert_no_cache_header, assert_shared_cache_header, KumaTestCase from ..urlresolvers import reverse from ..views import handler500 @pytest.fixture() def sitemaps(db, settings, tmpdir): - media_dir = tmpdir.mkdir('media') - locale_dir = media_dir.mkdir('sitemaps').mkdir('en-US') - sitemap_file = media_dir.join('sitemap.xml') - locale_file = locale_dir.join('sitemap.xml') - sitemap_file.write_text(""" + media_dir = tmpdir.mkdir("media") + locale_dir = media_dir.mkdir("sitemaps").mkdir("en-US") + sitemap_file = media_dir.join("sitemap.xml") + locale_file = locale_dir.join("sitemap.xml") + sitemap_file.write_text( + """ https://localhost:8000/sitemaps/en-US/sitemap.xml 2017-09-06T23:24:37+00:00 -""", 'utf8') - locale_file.write_text(""" +""", + "utf8", + ) + locale_file.write_text( + """ https://example.com/en-US/docs/foobar 2013-06-06 -""", 'utf8') +""", + "utf8", + ) return { - 'tmpdir': media_dir, - 'index': sitemap_file.read_text('utf8'), - 'locales': { - 'en-US': locale_file.read_text('utf8') - } + "tmpdir": media_dir, + "index": sitemap_file.read_text("utf8"), + "locales": {"en-US": locale_file.read_text("utf8")}, } @override_settings( DEBUG=False, DEBUG_PROPAGATE_EXCEPTIONS=False, - ADMINS=(('admin', 'admin@example.com'),), - ROOT_URLCONF='kuma.core.tests.logging_urls') + ADMINS=(("admin", "admin@example.com"),), + ROOT_URLCONF="kuma.core.tests.logging_urls", +) class LoggingTests(KumaTestCase): - logger = logging.getLogger('django') - suspicous_path = '/en-US/suspicious/' + logger = logging.getLogger("django") + suspicous_path = "/en-US/suspicious/" def setUp(self): super(LoggingTests, self).setUp() @@ -78,76 +80,71 @@ def test_mail_handler(self): assert 400 == response.status_code assert 1 == len(mail.outbox) - assert 'admin@example.com' in mail.outbox[0].to + assert "admin@example.com" in mail.outbox[0].to assert self.suspicous_path in mail.outbox[0].body class SoapboxViewsTest(KumaTestCase): - def test_global_home(self): - m = Message(message='Global', is_global=True, is_active=True, url='/') + m = Message(message="Global", is_global=True, is_active=True, url="/") m.save() - url = reverse('home') + url = reverse("home") r = self.client.get(url, follow=True) assert 200 == r.status_code doc = pq(r.content) - assert m.message == doc.find('div.global-notice').text() + assert m.message == doc.find("div.global-notice").text() def test_subsection(self): - m = Message(message='Search', is_global=False, is_active=True, - url='/search/') + m = Message(message="Search", is_global=False, is_active=True, url="/search/") m.save() - url = reverse('search') + url = reverse("search") r = self.client.get(url, follow=True) assert 200 == r.status_code doc = pq(r.content) - assert m.message == doc.find('div.global-notice').text() + assert m.message == doc.find("div.global-notice").text() - url = reverse('home') + url = reverse("home") r = self.client.get(url, follow=True) assert 200 == r.status_code doc = pq(r.content) - assert not doc.find('div.global-notice') + assert not doc.find("div.global-notice") def test_inactive(self): - m = Message(message='Search', is_global=False, is_active=False, - url='/search/') + m = Message(message="Search", is_global=False, is_active=False, url="/search/") m.save() - url = reverse('search') + url = reverse("search") r = self.client.get(url, follow=True) assert 200 == r.status_code doc = pq(r.content) - assert not doc.find('div.global-notice') + assert not doc.find("div.global-notice") class EventsRedirectTest(KumaTestCase): - def test_redirect_to_mozilla_org(self): - url = '/en-US/events' + url = "/en-US/events" response = self.client.get(url) assert 302 == response.status_code - assert 'https://mozilla.org/contribute/events' == response['Location'] + assert "https://mozilla.org/contribute/events" == response["Location"] -@pytest.mark.parametrize( - 'http_method', ['get', 'put', 'delete', 'options', 'head']) +@pytest.mark.parametrize("http_method", ["get", "put", "delete", "options", "head"]) def test_setting_language_cookie_disallowed_methods(client, http_method): - url = reverse('set-language-cookie') - response = getattr(client, http_method)(url, {'language': 'bn'}) + url = reverse("set-language-cookie") + response = getattr(client, http_method)(url, {"language": "bn"}) assert response.status_code == 405 assert_no_cache_header(response) def test_setting_language_cookie_working(client): - url = reverse('set-language-cookie') - response = client.post(url, {'language': 'bn'}) + url = reverse("set-language-cookie") + response = client.post(url, {"language": "bn"}) assert response.status_code == 204 assert_no_cache_header(response) @@ -155,100 +152,91 @@ def test_setting_language_cookie_working(client): # Check language cookie is set assert lang_cookie - assert lang_cookie.value == 'bn' + assert lang_cookie.value == "bn" # Check that the max-age from the cookie is the same as our settings - assert lang_cookie['max-age'] == settings.LANGUAGE_COOKIE_AGE + assert lang_cookie["max-age"] == settings.LANGUAGE_COOKIE_AGE def test_not_possible_to_set_non_locale_cookie(client): - url = reverse('set-language-cookie') - response = client.post(url, {'language': 'foo'}) + url = reverse("set-language-cookie") + response = client.post(url, {"language": "foo"}) assert response.status_code == 204 assert_no_cache_header(response) # No language cookie should be saved as `foo` is not a supported locale assert not response.client.cookies.get(settings.LANGUAGE_COOKIE_NAME) -@pytest.mark.parametrize('method', ['get', 'head']) +@pytest.mark.parametrize("method", ["get", "head"]) def test_sitemap(client, settings, sitemaps, db, method): - settings.MEDIA_ROOT = sitemaps['tmpdir'].realpath() - response = getattr(client, method)(reverse('sitemap')) + settings.MEDIA_ROOT = sitemaps["tmpdir"].realpath() + response = getattr(client, method)(reverse("sitemap")) assert response.status_code == 200 assert_shared_cache_header(response) - assert response['Content-Type'] == 'application/xml' - if method == 'get': - assert ''.join( - [chunk.decode() for chunk in response.streaming_content] - ) == sitemaps['index'] + assert response["Content-Type"] == "application/xml" + if method == "get": + assert ( + "".join([chunk.decode() for chunk in response.streaming_content]) + == sitemaps["index"] + ) -@pytest.mark.parametrize( - 'method', - ['post', 'put', 'delete', 'options', 'patch'] -) +@pytest.mark.parametrize("method", ["post", "put", "delete", "options", "patch"]) def test_sitemap_405s(client, db, method): - response = getattr(client, method)(reverse('sitemap')) + response = getattr(client, method)(reverse("sitemap")) assert response.status_code == 405 assert_shared_cache_header(response) -@pytest.mark.parametrize('method', ['get', 'head']) +@pytest.mark.parametrize("method", ["get", "head"]) def test_sitemaps(client, settings, sitemaps, db, method): - settings.MEDIA_ROOT = sitemaps['tmpdir'].realpath() + settings.MEDIA_ROOT = sitemaps["tmpdir"].realpath() response = getattr(client, method)( - reverse( - 'sitemaps', - kwargs={'path': 'sitemaps/en-US/sitemap.xml'} - ) + reverse("sitemaps", kwargs={"path": "sitemaps/en-US/sitemap.xml"}) ) assert response.status_code == 200 assert_shared_cache_header(response) - assert response['Content-Type'] == 'application/xml' - if method == 'get': - assert (''.join([chunk.decode() for chunk in response.streaming_content]) == - sitemaps['locales']['en-US']) + assert response["Content-Type"] == "application/xml" + if method == "get": + assert ( + "".join([chunk.decode() for chunk in response.streaming_content]) + == sitemaps["locales"]["en-US"] + ) -@pytest.mark.parametrize( - 'method', - ['post', 'put', 'delete', 'options', 'patch'] -) +@pytest.mark.parametrize("method", ["post", "put", "delete", "options", "patch"]) def test_sitemaps_405s(client, db, method): response = getattr(client, method)( - reverse( - 'sitemaps', - kwargs={'path': 'sitemaps/en-US/sitemap.xml'} - ) + reverse("sitemaps", kwargs={"path": "sitemaps/en-US/sitemap.xml"}) ) assert response.status_code == 405 assert_shared_cache_header(response) def test_ratelimit_429(client, db): - '''Custom 429 view is used for Ratelimited exception.''' - url = reverse('home') - with mock.patch('kuma.landing.views.render') as render: + """Custom 429 view is used for Ratelimited exception.""" + url = reverse("home") + with mock.patch("kuma.landing.views.render") as render: render.side_effect = Ratelimited() response = client.get(url) assert response.status_code == 429 - assert '429.html' in [t.name for t in response.templates] - assert response['Retry-After'] == '60' + assert "429.html" in [t.name for t in response.templates] + assert response["Retry-After"] == "60" assert_no_cache_header(response) def test_error_handler_minimal_request(rf, db, settings): - '''Error page renders if middleware hasn't added request members.''' + """Error page renders if middleware hasn't added request members.""" # Setup conditions for adding analytics with a flag check - settings.GOOGLE_ANALYTICS_ACCOUNT = 'UA-00000000-0' - Flag.objects.create(name='section_edit', authenticated=True) + settings.GOOGLE_ANALYTICS_ACCOUNT = "UA-00000000-0" + Flag.objects.create(name="section_edit", authenticated=True) # Create minimal request - request = rf.get('/en-US/docs/tags/Open Protocol') - assert not hasattr(request, 'LANGUAGE_CODE') - assert not hasattr(request, 'user') + request = rf.get("/en-US/docs/tags/Open Protocol") + assert not hasattr(request, "LANGUAGE_CODE") + assert not hasattr(request, "user") # Generate the 500 page - exception = Exception('Something went wrong.') + exception = Exception("Something went wrong.") response = handler500(request, exception) assert response.status_code == 500 - assert b'Internal Server Error' in response.content + assert b"Internal Server Error" in response.content diff --git a/kuma/core/urlresolvers.py b/kuma/core/urlresolvers.py index 01a7b1f1756..ee437ff3790 100644 --- a/kuma/core/urlresolvers.py +++ b/kuma/core/urlresolvers.py @@ -1,11 +1,10 @@ - - import re from django.conf import settings from django.urls import ( LocaleRegexURLResolver as DjangoLocaleRegexURLResolver, - reverse as django_reverse) + reverse as django_reverse, +) from django.utils import translation from .i18n import get_language @@ -32,9 +31,8 @@ def regex(self): if language_code not in self._regex_dict: # Kuma: Do not allow an implied default language assert self.prefix_default_language - regex_string = '^%s/' % language_code - self._regex_dict[language_code] = re.compile( - regex_string, re.UNICODE) + regex_string = "^%s/" % language_code + self._regex_dict[language_code] = re.compile(regex_string, re.UNICODE) return self._regex_dict[language_code] @@ -53,20 +51,25 @@ def i18n_patterns(*urls, **kwargs): * Use our customized LocaleRegexURLResolver. """ assert settings.USE_I18N - prefix_default_language = kwargs.pop('prefix_default_language', True) - assert not kwargs, 'Unexpected kwargs for i18n_patterns(): %s' % kwargs + prefix_default_language = kwargs.pop("prefix_default_language", True) + assert not kwargs, "Unexpected kwargs for i18n_patterns(): %s" % kwargs # Assumed to be True in: # kuma.core.i18n.activate_language_from_request # kuma.core.middleware.LocaleMiddleware - assert prefix_default_language, ( - 'Kuma does not support prefix_default_language=False') - return [LocaleRegexURLResolver(list(urls), - prefix_default_language=prefix_default_language)] - - -def reverse(viewname, urlconf=None, args=None, kwargs=None, - current_app=None, locale=None): + assert ( + prefix_default_language + ), "Kuma does not support prefix_default_language=False" + return [ + LocaleRegexURLResolver( + list(urls), prefix_default_language=prefix_default_language + ) + ] + + +def reverse( + viewname, urlconf=None, args=None, kwargs=None, current_app=None, locale=None +): """Wraps Django's reverse to prepend the requested locale. Keyword Arguments: * locale - Use this locale prefix rather than the current active locale. @@ -79,11 +82,17 @@ def reverse(viewname, urlconf=None, args=None, kwargs=None, """ if locale: with translation.override(locale): - return django_reverse(viewname, urlconf=urlconf, args=args, - kwargs=kwargs, current_app=current_app) + return django_reverse( + viewname, + urlconf=urlconf, + args=args, + kwargs=kwargs, + current_app=current_app, + ) else: - return django_reverse(viewname, urlconf=urlconf, args=args, - kwargs=kwargs, current_app=current_app) + return django_reverse( + viewname, urlconf=urlconf, args=args, kwargs=kwargs, current_app=current_app + ) def find_supported(ranked): @@ -94,7 +103,7 @@ def find_supported(ranked): if lang in langs: return langs[lang] # Add derived language tags to the end of the list as a fallback. - pre = '-'.join(lang.split('-')[0:-1]) + pre = "-".join(lang.split("-")[0:-1]) if pre: ranked.append((pre, None)) # Couldn't find any acceptable locale. @@ -107,10 +116,10 @@ def split_path(path): locale will be empty if it isn't found. """ - path = path.lstrip('/') + path = path.lstrip("/") # Use partition instead of split since it always returns 3 parts - first, _, rest = path.partition('/') + first, _, rest = path.partition("/") # Treat locale as a single-item ranked list. lang = find_supported([(first, 1.0)]) @@ -118,4 +127,4 @@ def split_path(path): if lang: return lang, rest else: - return '', path + return "", path diff --git a/kuma/core/utils.py b/kuma/core/utils.py index 045da390ec7..1f1097737ac 100644 --- a/kuma/core/utils.py +++ b/kuma/core/utils.py @@ -1,5 +1,3 @@ - - import datetime import hashlib import logging @@ -28,7 +26,7 @@ from .exceptions import DateTimeFormatError -log = logging.getLogger('kuma.core.utils') +log = logging.getLogger("kuma.core.utils") def to_html(pq): @@ -41,7 +39,7 @@ def to_html(pq): "iframe" element would be "". """ - return pq.html(method='html') + return pq.html(method="html") def is_wiki(request): @@ -49,15 +47,12 @@ def is_wiki(request): def redirect_to_wiki(request, permanent=True): - request.META['HTTP_HOST'] = settings.WIKI_HOST + request.META["HTTP_HOST"] = settings.WIKI_HOST return redirect(request.build_absolute_uri(), permanent=permanent) def is_untrusted(request): - return request.get_host() in ( - settings.ATTACHMENT_ORIGIN, - settings.ATTACHMENT_HOST, - ) + return request.get_host() in (settings.ATTACHMENT_ORIGIN, settings.ATTACHMENT_HOST,) def paginate(request, queryset, per_page=20): @@ -66,7 +61,7 @@ def paginate(request, queryset, per_page=20): # Get the page from the request, make sure it's an int. try: - page = int(request.GET.get('page', 1)) + page = int(request.GET.get("page", 1)) except ValueError: page = 1 @@ -78,12 +73,13 @@ def paginate(request, queryset, per_page=20): base = request.build_absolute_uri(request.path) - items = [(k, v) for k in request.GET if k != 'page' - for v in request.GET.getlist(k) if v] + items = [ + (k, v) for k in request.GET if k != "page" for v in request.GET.getlist(k) if v + ] qsa = urlencode(items) - paginated.url = f'{base}?{qsa}' + paginated.url = f"{base}?{qsa}" return paginated @@ -97,17 +93,22 @@ def smart_int(string, fallback=0): def strings_are_translated(strings, locale): # http://stackoverflow.com/a/24339946/571420 - pofile_path = os.path.join(settings.ROOT, 'locale', locale, 'LC_MESSAGES', - 'django.po') + pofile_path = os.path.join( + settings.ROOT, "locale", locale, "LC_MESSAGES", "django.po" + ) try: po = pofile(pofile_path) except IOError: # in case the file doesn't exist or couldn't be parsed return False all_strings_translated = True for string in strings: - if not any(e for e in po if e.msgid == string and - (e.translated() and 'fuzzy' not in e.flags) and - not e.obsolete): + if not any( + e + for e in po + if e.msgid == string + and (e.translated() and "fuzzy" not in e.flags) + and not e.obsolete + ): all_strings_translated = False return all_strings_translated @@ -123,9 +124,7 @@ def generate_filename_and_delete_previous(ffile, name, before_delete=None): # wasteful and dirty. But, I can't think of another way to get # to the original field's value. Should be cached, though. # see also - http://code.djangoproject.com/ticket/11663#comment:10 - orig_instance = ffile.instance.__class__.objects.get( - id=ffile.instance.id - ) + orig_instance = ffile.instance.__class__.objects.get(id=ffile.instance.id) orig_field_file = getattr(orig_instance, ffile.field.name) orig_filename = orig_field_file.name @@ -170,8 +169,8 @@ def parse_tags(tagstring, sorted=True): # Special case - if there are no commas or double quotes in the # input, we don't *do* a recall... I mean, we know we only need to # split on spaces. - if ',' not in tagstring and '"' not in tagstring: - words = list(split_strip(tagstring, ' ')) + if "," not in tagstring and '"' not in tagstring: + words = list(split_strip(tagstring, " ")) if sorted: words.sort() return words @@ -189,7 +188,7 @@ def parse_tags(tagstring, sorted=True): c = next(i) if c == '"': if buffer: - to_be_split.append(''.join(buffer)) + to_be_split.append("".join(buffer)) buffer = [] # Find the matching quote open_quote = True @@ -198,27 +197,27 @@ def parse_tags(tagstring, sorted=True): buffer.append(c) c = next(i) if buffer: - word = ''.join(buffer).strip() + word = "".join(buffer).strip() if word: words.append(word) buffer = [] open_quote = False else: - if not saw_loose_comma and c == ',': + if not saw_loose_comma and c == ",": saw_loose_comma = True buffer.append(c) except StopIteration: # If we were parsing an open quote which was never closed treat # the buffer as unquoted. if buffer: - if open_quote and ',' in buffer: + if open_quote and "," in buffer: saw_loose_comma = True - to_be_split.append(''.join(buffer)) + to_be_split.append("".join(buffer)) if to_be_split: if saw_loose_comma: - delimiter = ',' + delimiter = "," else: - delimiter = ' ' + delimiter = " " for chunk in to_be_split: words.extend(split_strip(chunk, delimiter)) words = list(words) @@ -264,8 +263,15 @@ def chord_flow(pre_task, tasks, post_task): return chain(pre_task, chord(header=tasks, body=post_task)) -def get_unique(content_type, object_pk, name=None, request=None, - ip=None, user_agent=None, user=None): +def get_unique( + content_type, + object_pk, + name=None, + request=None, + ip=None, + user_agent=None, + user=None, +): """Extract a set of unique identifiers from the request. This set will be made up of one of the following combinations, depending @@ -280,20 +286,20 @@ def get_unique(content_type, object_pk, name=None, request=None, ip = user_agent = None else: user = None - ip = request.META.get('REMOTE_ADDR', '') - user_agent = request.META.get('HTTP_USER_AGENT', '')[:255] + ip = request.META.get("REMOTE_ADDR", "") + user_agent = request.META.get("HTTP_USER_AGENT", "")[:255] # HACK: Build a hash of the fields that should be unique, let MySQL # chew on that for a unique index. Note that any changes to this algo # will create all new unique hashes that don't match any existing ones. - hash_text = '\n'.join( + hash_text = "\n".join( ( content_type.pk, object_pk, - name or '', + name or "", ip, user_agent, - user.pk if user else 'None', + user.pk if user else "None", ) ) unique_hash = hashlib.md5(hash_text.encode()).hexdigest() @@ -312,8 +318,9 @@ def urlparams(url_, fragment=None, query_dict=None, **query): fragment = fragment if fragment is not None else url_.fragment q = url_.query - new_query_dict = (QueryDict(smart_bytes(q), mutable=True) if - q else QueryDict('', mutable=True)) + new_query_dict = ( + QueryDict(smart_bytes(q), mutable=True) if q else QueryDict("", mutable=True) + ) if query_dict: for k, l in query_dict.lists(): new_query_dict[k] = None # Replace, don't append. @@ -327,13 +334,16 @@ def urlparams(url_, fragment=None, query_dict=None, **query): else: new_query_dict[k] = v - query_string = urlencode([(k, v) for k, l in new_query_dict.lists() for - v in l if v is not None]) - new = ParseResult(url_.scheme, url_.netloc, url_.path, url_.params, query_string, fragment) + query_string = urlencode( + [(k, v) for k, l in new_query_dict.lists() for v in l if v is not None] + ) + new = ParseResult( + url_.scheme, url_.netloc, url_.path, url_.params, query_string, fragment + ) return new.geturl() -def format_date_time(request, value, format='shortdatetime'): +def format_date_time(request, value, format="shortdatetime"): """ Returns date/time formatted using babel's locale settings. Uses the timezone from settings.py @@ -341,8 +351,7 @@ def format_date_time(request, value, format='shortdatetime'): if not isinstance(value, datetime.datetime): if isinstance(value, datetime.date): # Turn a date into a datetime - value = datetime.datetime.combine(value, - datetime.datetime.min.time()) + value = datetime.datetime.combine(value, datetime.datetime.min.time()) else: # Expecting datetime value raise ValueError @@ -367,7 +376,8 @@ def format_date_time(request, value, format='shortdatetime'): # e.g. bug #1247086 # we fall back formatting the value with the default language code formatted = format_date_value( - value, tzvalue, language_to_locale(settings.LANGUAGE_CODE), format) + value, tzvalue, language_to_locale(settings.LANGUAGE_CODE), format + ) return formatted, tzvalue @@ -381,22 +391,20 @@ def _get_request_locale(request): def format_date_value(value, tzvalue, locale, format): - if format == 'shortdatetime': + if format == "shortdatetime": # Check if the date is today if value.toordinal() == datetime.date.today().toordinal(): - formatted = dates.format_time(tzvalue, format='short', - locale=locale) - return _('Today at %s') % formatted + formatted = dates.format_time(tzvalue, format="short", locale=locale) + return _("Today at %s") % formatted else: - return dates.format_datetime(tzvalue, format='short', - locale=locale) - elif format == 'longdatetime': - return dates.format_datetime(tzvalue, format='long', locale=locale) - elif format == 'date': + return dates.format_datetime(tzvalue, format="short", locale=locale) + elif format == "longdatetime": + return dates.format_datetime(tzvalue, format="long", locale=locale) + elif format == "date": return dates.format_date(tzvalue, locale=locale) - elif format == 'time': + elif format == "time": return dates.format_time(tzvalue, locale=locale) - elif format == 'datetime': + elif format == "datetime": return dates.format_datetime(tzvalue, locale=locale) else: # Unknown format @@ -413,7 +421,7 @@ def language_to_locale(language_code): https://docs.djangoproject.com/en/1.11/topics/i18n/#definitions """ - return language_code.replace('-', '_') + return language_code.replace("-", "_") def add_shared_cache_control(response, **kwargs): @@ -427,17 +435,18 @@ def add_shared_cache_control(response, **kwargs): cache for the default perioid of time - public - Allow intermediate proxies to cache response """ - nocache = (response.has_header('Cache-Control') and - ('no-cache' in response['Cache-Control'] or - 'no-store' in response['Cache-Control'])) + nocache = response.has_header("Cache-Control") and ( + "no-cache" in response["Cache-Control"] + or "no-store" in response["Cache-Control"] + ) if nocache: return # Set the default values. cc_kwargs = { - 'public': True, - 'max_age': 0, - 's_maxage': settings.CACHE_CONTROL_DEFAULT_SHARED_MAX_AGE + "public": True, + "max_age": 0, + "s_maxage": settings.CACHE_CONTROL_DEFAULT_SHARED_MAX_AGE, } # Override the default values and/or add new ones. cc_kwargs.update(kwargs) @@ -485,8 +494,8 @@ def requests_retry_session( status_forcelist=status_forcelist, ) adapter = HTTPAdapter(max_retries=retry) - session.mount('http://', adapter) - session.mount('https://', adapter) + session.mount("http://", adapter) + session.mount("https://", adapter) return session @@ -522,9 +531,11 @@ def safer_pyquery(*args, **kwargs): # This "if" statement is exactly what PyQuery's constructor does. # We'll run it ourselves once and if it matches, "ruin" it by # injecting that extra space. - if (len(args) >= 1 and - isinstance(args[0], str) and - args[0].split('://', 1)[0] in ('http', 'https')): - args = (f' {args[0]}',) + args[1:] + if ( + len(args) >= 1 + and isinstance(args[0], str) + and args[0].split("://", 1)[0] in ("http", "https") + ): + args = (f" {args[0]}",) + args[1:] return pq(*args, **kwargs) diff --git a/kuma/core/validators.py b/kuma/core/validators.py index d92dc464300..ed2ff8f273a 100644 --- a/kuma/core/validators.py +++ b/kuma/core/validators.py @@ -12,21 +12,19 @@ # javascript identifier unicode categories and "exceptional" chars # ------------------------------------------------------------------------------ -valid_jsid_categories_start = frozenset([ - 'Lu', 'Ll', 'Lt', 'Lm', 'Lo', 'Nl' -]) +valid_jsid_categories_start = frozenset(["Lu", "Ll", "Lt", "Lm", "Lo", "Nl"]) -valid_jsid_categories = frozenset([ - 'Lu', 'Ll', 'Lt', 'Lm', 'Lo', 'Nl', 'Mn', 'Mc', 'Nd', 'Pc' -]) +valid_jsid_categories = frozenset( + ["Lu", "Ll", "Lt", "Lm", "Lo", "Nl", "Mn", "Mc", "Nd", "Pc"] +) -valid_jsid_chars = ('$', '_') +valid_jsid_chars = ("$", "_") # ------------------------------------------------------------------------------ # regex to find array[index] patterns # ------------------------------------------------------------------------------ -array_index_regex = re.compile(r'\[[0-9]+\]$') +array_index_regex = re.compile(r"\[[0-9]+\]$") has_valid_array_index = array_index_regex.search replace_array_index = array_index_regex.sub @@ -35,27 +33,78 @@ # javascript reserved words -- including keywords and null/boolean literals # ------------------------------------------------------------------------------ -is_reserved_js_word = frozenset([ - - 'abstract', 'boolean', 'break', 'byte', 'case', 'catch', 'char', 'class', - 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'double', - 'else', 'enum', 'export', 'extends', 'false', 'final', 'finally', 'float', - 'for', 'function', 'goto', 'if', 'implements', 'import', 'in', 'instanceof', - 'int', 'interface', 'long', 'native', 'new', 'null', 'package', 'private', - 'protected', 'public', 'return', 'short', 'static', 'super', 'switch', - 'synchronized', 'this', 'throw', 'throws', 'transient', 'true', 'try', - 'typeof', 'var', 'void', 'volatile', 'while', 'with', - - # potentially reserved in a future version of the ES5 standard - # 'let', 'yield' -]).__contains__ +is_reserved_js_word = frozenset( + [ + "abstract", + "boolean", + "break", + "byte", + "case", + "catch", + "char", + "class", + "const", + "continue", + "debugger", + "default", + "delete", + "do", + "double", + "else", + "enum", + "export", + "extends", + "false", + "final", + "finally", + "float", + "for", + "function", + "goto", + "if", + "implements", + "import", + "in", + "instanceof", + "int", + "interface", + "long", + "native", + "new", + "null", + "package", + "private", + "protected", + "public", + "return", + "short", + "static", + "super", + "switch", + "synchronized", + "this", + "throw", + "throws", + "transient", + "true", + "try", + "typeof", + "var", + "void", + "volatile", + "while", + "with", + # potentially reserved in a future version of the ES5 standard + # 'let', 'yield' + ] +).__contains__ # ------------------------------------------------------------------------------ # the core validation functions # ------------------------------------------------------------------------------ -def valid_javascript_identifier(identifier, escape='\\u', ucd_cat=category): +def valid_javascript_identifier(identifier, escape="\\u", ucd_cat=category): """Return whether the given ``id`` is a valid Javascript identifier.""" if not identifier: @@ -63,7 +112,7 @@ def valid_javascript_identifier(identifier, escape='\\u', ucd_cat=category): if not isinstance(identifier, str): try: - identifier = str(identifier, 'utf-8') + identifier = str(identifier, "utf-8") except UnicodeDecodeError: return False @@ -78,25 +127,26 @@ def valid_javascript_identifier(identifier, escape='\\u', ucd_cat=category): if len(segment) < 4: return False try: - add_char(chr(int('0x' + segment[:4], 16))) + add_char(chr(int("0x" + segment[:4], 16))) except Exception: return False add_char(segment[4:]) - identifier = ''.join(new) + identifier = "".join(new) if is_reserved_js_word(identifier): return False first_char = identifier[0] - if not ((first_char in valid_jsid_chars) or - (ucd_cat(first_char) in valid_jsid_categories_start)): + if not ( + (first_char in valid_jsid_chars) + or (ucd_cat(first_char) in valid_jsid_categories_start) + ): return False for char in identifier[1:]: - if not ((char in valid_jsid_chars) or - (ucd_cat(char) in valid_jsid_categories)): + if not ((char in valid_jsid_chars) or (ucd_cat(char) in valid_jsid_categories)): return False return True @@ -105,11 +155,11 @@ def valid_javascript_identifier(identifier, escape='\\u', ucd_cat=category): def valid_jsonp_callback_value(value): """Return whether the given ``value`` can be used as a JSON-P callback.""" - for identifier in value.split('.'): - while '[' in identifier: + for identifier in value.split("."): + while "[" in identifier: if not has_valid_array_index(identifier): return False - identifier = replace_array_index('', identifier) + identifier = replace_array_index("", identifier) if not valid_javascript_identifier(identifier): return False diff --git a/kuma/core/views.py b/kuma/core/views.py index 5c1ca0c3c79..6baea1bb959 100644 --- a/kuma/core/views.py +++ b/kuma/core/views.py @@ -1,5 +1,3 @@ - - from django.conf import settings from django.contrib.auth.models import AnonymousUser from django.http import HttpResponse @@ -21,11 +19,11 @@ def _error_page(request, status): Sometimes, an error is raised by a middleware, and the request is not fully populated with a user or language code. Add in good defaults. """ - if not hasattr(request, 'user'): + if not hasattr(request, "user"): request.user = AnonymousUser() - if not hasattr(request, 'LANGUAGE_CODE'): - request.LANGUAGE_CODE = 'en-US' - return render(request, '%d.html' % status, status=status) + if not hasattr(request, "LANGUAGE_CODE"): + request.LANGUAGE_CODE = "en-US" + return render(request, "%d.html" % status, status=status) @never_cache @@ -37,26 +35,27 @@ def set_language(request): if lang_code and lang_code in get_kuma_languages(): - response.set_cookie(key=settings.LANGUAGE_COOKIE_NAME, - value=lang_code, - max_age=settings.LANGUAGE_COOKIE_AGE, - path=settings.LANGUAGE_COOKIE_PATH, - domain=settings.LANGUAGE_COOKIE_DOMAIN, - ) + response.set_cookie( + key=settings.LANGUAGE_COOKIE_NAME, + value=lang_code, + max_age=settings.LANGUAGE_COOKIE_AGE, + path=settings.LANGUAGE_COOKIE_PATH, + domain=settings.LANGUAGE_COOKIE_DOMAIN, + ) return response -handler403 = lambda request, exception=None: _error_page(request, 403) -handler404 = lambda request, exception=None: _error_page(request, 404) -handler500 = lambda request, exception=None: _error_page(request, 500) +handler403 = lambda request, exception=None: _error_page(request, 403) # noqa: E731 +handler404 = lambda request, exception=None: _error_page(request, 404) # noqa: E731 +handler500 = lambda request, exception=None: _error_page(request, 500) # noqa: E731 @never_cache def rate_limited(request, exception): """Render a rate-limited exception.""" - response = render(request, '429.html', status=429) - response['Retry-After'] = '60' + response = render(request, "429.html", status=429) + response["Retry-After"] = "60" return response diff --git a/kuma/dashboards/forms.py b/kuma/dashboards/forms.py index 188ef400af8..042c522ed02 100644 --- a/kuma/dashboards/forms.py +++ b/kuma/dashboards/forms.py @@ -1,5 +1,3 @@ - - from django import forms from django.conf import settings from django.utils.translation import ugettext_lazy as _ @@ -7,13 +5,13 @@ from kuma.core.form_fields import StrippedCharField -LANG_CHOICES = [('', _('All Locales'))] + settings.SORTED_LANGUAGES +LANG_CHOICES = [("", _("All Locales"))] + settings.SORTED_LANGUAGES PERIOD_CHOICES = [ - ('', _('None')), - ('hour', _('Hour')), - ('day', _('Day')), - ('week', _('Week')), - ('month', _('30 days')), + ("", _("None")), + ("hour", _("Hour")), + ("day", _("Day")), + ("week", _("Week")), + ("month", _("30 days")), ] @@ -22,9 +20,9 @@ class RevisionDashboardForm(forms.Form): KNOWN_AUTHORS = 1 UNKNOWN_AUTHORS = 2 AUTHOR_CHOICES = [ - (ALL_AUTHORS, _('All Authors')), - (KNOWN_AUTHORS, _('Known Authors')), - (UNKNOWN_AUTHORS, _('Unknown Authors')), + (ALL_AUTHORS, _("All Authors")), + (KNOWN_AUTHORS, _("Known Authors")), + (UNKNOWN_AUTHORS, _("Unknown Authors")), ] locale = forms.ChoiceField( @@ -32,28 +30,29 @@ class RevisionDashboardForm(forms.Form): # Required for non-translations, which is # enforced in Document.clean(). required=False, - label=_('Locale:')) + label=_("Locale:"), + ) user = StrippedCharField( - min_length=1, max_length=255, - required=False, - label=_('User:')) + min_length=1, max_length=255, required=False, label=_("User:") + ) topic = StrippedCharField( - min_length=1, max_length=255, - required=False, - label=_('Topic:')) + min_length=1, max_length=255, required=False, label=_("Topic:") + ) start_date = forms.DateField( - required=False, label=_('Start Date:'), - input_formats=['%m/%d/%Y'], - widget=forms.TextInput(attrs={'pattern': r'\d{1,2}/\d{1,2}/\d{4}'})) + required=False, + label=_("Start Date:"), + input_formats=["%m/%d/%Y"], + widget=forms.TextInput(attrs={"pattern": r"\d{1,2}/\d{1,2}/\d{4}"}), + ) end_date = forms.DateField( - required=False, label=_('End Date:'), - input_formats=['%m/%d/%Y'], - widget=forms.TextInput(attrs={'pattern': r'\d{1,2}/\d{1,2}/\d{4}'})) - preceding_period = forms.ChoiceField( - choices=PERIOD_CHOICES, required=False, - label=_('Preceding Period:')) + label=_("End Date:"), + input_formats=["%m/%d/%Y"], + widget=forms.TextInput(attrs={"pattern": r"\d{1,2}/\d{1,2}/\d{4}"}), + ) + preceding_period = forms.ChoiceField( + choices=PERIOD_CHOICES, required=False, label=_("Preceding Period:") + ) authors = forms.ChoiceField( - choices=AUTHOR_CHOICES, - required=False, - label=_('Authors')) + choices=AUTHOR_CHOICES, required=False, label=_("Authors") + ) diff --git a/kuma/dashboards/jobs.py b/kuma/dashboards/jobs.py index f5b474abdfa..76b6412b7a1 100644 --- a/kuma/dashboards/jobs.py +++ b/kuma/dashboards/jobs.py @@ -1,14 +1,15 @@ - - from kuma.core.jobs import KumaJob -from .utils import (spam_dashboard_historical_stats, - spam_dashboard_recent_events, - spam_day_stats) +from .utils import ( + spam_dashboard_historical_stats, + spam_dashboard_recent_events, + spam_day_stats, +) class SpamDayStats(KumaJob): """Cache spam stats for multiple days.""" + lifetime = 60 * 60 * 24 * 7 fetch_on_miss = True version = 1 @@ -19,6 +20,7 @@ def fetch(self, day): class SpamDashboardHistoricalStats(KumaJob): """Cache historical spam stats for multiple days.""" + lifetime = 60 * 60 * 24 fetch_on_miss = False version = 1 @@ -29,10 +31,10 @@ def fetch(self, end_date): class SpamDashboardRecentEvents(KumaJob): """Cache recent event data for a very short time.""" + lifetime = 60 fetch_on_miss = True version = 1 def fetch(self, start_date, end_date): - return spam_dashboard_recent_events(start=start_date, - end=end_date) + return spam_dashboard_recent_events(start=start_date, end=end_date) diff --git a/kuma/dashboards/tests/test_views.py b/kuma/dashboards/tests/test_views.py index a59d8f831e2..28b5019aa68 100644 --- a/kuma/dashboards/tests/test_views.py +++ b/kuma/dashboards/tests/test_views.py @@ -1,5 +1,3 @@ - - import datetime import json from unittest import mock @@ -13,16 +11,22 @@ from pyquery import PyQuery as pq from waffle.testutils import override_switch -from kuma.core.tests import (assert_no_cache_header, - assert_redirect_to_wiki, - assert_shared_cache_header) +from kuma.core.tests import ( + assert_no_cache_header, + assert_redirect_to_wiki, + assert_shared_cache_header, +) from kuma.core.urlresolvers import reverse from kuma.core.utils import to_html, urlparams from kuma.dashboards.forms import RevisionDashboardForm from kuma.users.tests import create_document, SampleRevisionsMixin, UserTestCase -from kuma.wiki.models import (Document, DocumentDeletionLog, - DocumentSpamAttempt, Revision, - RevisionAkismetSubmission) +from kuma.wiki.models import ( + Document, + DocumentDeletionLog, + DocumentSpamAttempt, + Revision, + RevisionAkismetSubmission, +) REVS_PER_USER = 3 # Number of revisions per user in dashboard_revisions @@ -36,30 +40,40 @@ def dashboard_revisions(wiki_user, wiki_user_2, wiki_user_3): day = datetime.datetime(2018, 4, 1) users = wiki_user, wiki_user_2, wiki_user_3 languages = { - 'de': ("%s's Dokument", '

    Zweiter Schnitt von %s

    '), - 'es': ('El Documento de %s', '

    Segunda edición por %s

    '), - 'fr': ('Le Document de %s', '

    Deuxième édition par %s

    '), + "de": ("%s's Dokument", "

    Zweiter Schnitt von %s

    "), + "es": ("El Documento de %s", "

    Segunda edición por %s

    "), + "fr": ("Le Document de %s", "

    Deuxième édition par %s

    "), } for user, lang in zip(users, sorted(languages.keys())): - en_title = user.username + ' Document' + en_title = user.username + " Document" en_doc = Document.objects.create( - locale='en-US', slug=user.username + '-doc', title=en_title) + locale="en-US", slug=user.username + "-doc", title=en_title + ) first_rev = Revision.objects.create( - document=en_doc, creator=user, title=en_title, - content='

    First edit by %s

    ' % user.username, - created=day) - trans_title, trans_content = ( - fmt % user.username for fmt in languages[lang]) + document=en_doc, + creator=user, + title=en_title, + content="

    First edit by %s

    " % user.username, + created=day, + ) + trans_title, trans_content = (fmt % user.username for fmt in languages[lang]) trans_doc = Document.objects.create( - locale=lang, slug=user.username + '-doc', title=trans_title) + locale=lang, slug=user.username + "-doc", title=trans_title + ) trans_rev = Revision.objects.create( - document=trans_doc, creator=user, title=trans_title, + document=trans_doc, + creator=user, + title=trans_title, content=trans_content, - created=day + datetime.timedelta(seconds=2 * 60 * 60)) + created=day + datetime.timedelta(seconds=2 * 60 * 60), + ) third_rev = Revision.objects.create( - document=en_doc, creator=user, title=en_title, - content='

    Third edit by %s

    ' % user.username, - created=day + datetime.timedelta(seconds=3 * 60 * 60)) + document=en_doc, + creator=user, + title=en_title, + content="

    Third edit by %s

    " % user.username, + created=day + datetime.timedelta(seconds=3 * 60 * 60), + ) revisions.extend((first_rev, trans_rev, third_rev)) day += datetime.timedelta(days=1) @@ -70,149 +84,166 @@ def dashboard_revisions(wiki_user, wiki_user_2, wiki_user_3): @pytest.fixture def known_author(wiki_user): """Add wiki_user to the Known Users group.""" - group = Group.objects.create(name='Known Authors') + group = Group.objects.create(name="Known Authors") group.user_set.add(wiki_user) return wiki_user def test_revisions_not_logged_in(root_doc, client): """A user who is not logged in can't see the revisions dashboard.""" - url = reverse('dashboards.revisions') + url = reverse("dashboards.revisions") response = client.get(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 302 - assert response['Location'] == '/en-US/users/signin?next={}'.format(url) + assert response["Location"] == "/en-US/users/signin?next={}".format(url) assert_no_cache_header(response) def test_revisions(root_doc, user_client): """The revision dashboard works for logged in users.""" - response = user_client.get(reverse('dashboards.revisions'), - HTTP_HOST=settings.WIKI_HOST) + response = user_client.get( + reverse("dashboards.revisions"), HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 200 - assert 'Cache-Control' in response + assert "Cache-Control" in response assert_no_cache_header(response) - assert 'text/html' in response['Content-Type'] - assert ('dashboards/revisions.html' in - (template.name for template in response.templates)) + assert "text/html" in response["Content-Type"] + assert "dashboards/revisions.html" in ( + template.name for template in response.templates + ) def test_revisions_list_via_AJAX(dashboard_revisions, user_client): """The full list of revisions can be returned via AJAX.""" - response = user_client.get(reverse('dashboards.revisions'), - HTTP_HOST=settings.WIKI_HOST, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') + response = user_client.get( + reverse("dashboards.revisions"), + HTTP_HOST=settings.WIKI_HOST, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) assert response.status_code == 200 page = pq(response.content) - rev_rows = page.find('.dashboard-row') + rev_rows = page.find(".dashboard-row") assert rev_rows.length == len(dashboard_revisions) == 3 * REVS_PER_USER # Revisions are in order, most recent first for rev_row, revision in zip(rev_rows, dashboard_revisions): - assert int(rev_row.attrib['data-revision-id']) == revision.id + assert int(rev_row.attrib["data-revision-id"]) == revision.id -@pytest.mark.parametrize('switch', (True, False)) -@pytest.mark.parametrize('is_admin', (True, False)) -def test_revisions_show_ips_button(switch, is_admin, root_doc, user_client, - admin_client): +@pytest.mark.parametrize("switch", (True, False)) +@pytest.mark.parametrize("is_admin", (True, False)) +def test_revisions_show_ips_button( + switch, is_admin, root_doc, user_client, admin_client +): """Toggle IPs button appears for admins when the switch is active.""" client = admin_client if is_admin else user_client - with override_switch('store_revision_ips', active=switch): - response = client.get(reverse('dashboards.revisions'), - HTTP_HOST=settings.WIKI_HOST) + with override_switch("store_revision_ips", active=switch): + response = client.get( + reverse("dashboards.revisions"), HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 200 page = pq(response.content) - ip_button = page.find('button#show_ips_btn') + ip_button = page.find("button#show_ips_btn") assert len(ip_button) == (1 if (switch and is_admin) else 0) -@pytest.mark.parametrize('has_perm', (True, False)) -def test_revisions_show_spam_submission_button(has_perm, root_doc, wiki_user, - user_client): +@pytest.mark.parametrize("has_perm", (True, False)) +def test_revisions_show_spam_submission_button( + has_perm, root_doc, wiki_user, user_client +): """Submit as spam button appears when the user has the permission.""" if has_perm: - content_type = ContentType.objects.get_for_model( - RevisionAkismetSubmission) + content_type = ContentType.objects.get_for_model(RevisionAkismetSubmission) perm = Permission.objects.get( - codename='add_revisionakismetsubmission', - content_type=content_type) + codename="add_revisionakismetsubmission", content_type=content_type + ) wiki_user.user_permissions.add(perm) - response = user_client.get(reverse('dashboards.revisions'), - HTTP_HOST=settings.WIKI_HOST) + response = user_client.get( + reverse("dashboards.revisions"), HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 200 page = pq(response.content) - spam_report_button = page.find('.spam-ham-button') + spam_report_button = page.find(".spam-ham-button") assert len(spam_report_button) == (1 if has_perm else 0) def test_revisions_locale_filter(dashboard_revisions, user_client): """Revisions can be filtered by locale.""" - url = urlparams(reverse('dashboards.revisions', locale='fr'), - locale='fr') - response = user_client.get(url, HTTP_HOST=settings.WIKI_HOST, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') + url = urlparams(reverse("dashboards.revisions", locale="fr"), locale="fr") + response = user_client.get( + url, HTTP_HOST=settings.WIKI_HOST, HTTP_X_REQUESTED_WITH="XMLHttpRequest" + ) assert response.status_code == 200 page = pq(response.content) - revisions = page.find('.dashboard-row') + revisions = page.find(".dashboard-row") assert revisions.length == 1 - locale = to_html(revisions.find('.locale')) - assert locale == 'fr' + locale = to_html(revisions.find(".locale")) + assert locale == "fr" def test_revisions_creator_filter(dashboard_revisions, user_client): """Revisions can be filtered by a username.""" - url = urlparams(reverse('dashboards.revisions'), user='wiki_user_2') - response = user_client.get(url, HTTP_HOST=settings.WIKI_HOST, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') + url = urlparams(reverse("dashboards.revisions"), user="wiki_user_2") + response = user_client.get( + url, HTTP_HOST=settings.WIKI_HOST, HTTP_X_REQUESTED_WITH="XMLHttpRequest" + ) assert response.status_code == 200 page = pq(response.content) - revisions = page.find('.dashboard-row') + revisions = page.find(".dashboard-row") assert revisions.length == REVS_PER_USER - authors = revisions.find('.dashboard-author') + authors = revisions.find(".dashboard-author") assert authors.length == REVS_PER_USER for author in authors: - assert author.text_content().strip() == 'wiki_user_2' + assert author.text_content().strip() == "wiki_user_2" def test_revisions_topic_filter(dashboard_revisions, user_client): """Revisions can be filtered by topic (the document slug).""" - url = urlparams(reverse('dashboards.revisions'), topic='wiki_user_2-doc') - response = user_client.get(url, HTTP_HOST=settings.WIKI_HOST, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') + url = urlparams(reverse("dashboards.revisions"), topic="wiki_user_2-doc") + response = user_client.get( + url, HTTP_HOST=settings.WIKI_HOST, HTTP_X_REQUESTED_WITH="XMLHttpRequest" + ) assert response.status_code == 200 page = pq(response.content) - revisions = page.find('.dashboard-row') + revisions = page.find(".dashboard-row") assert revisions.length == REVS_PER_USER - slugs = revisions.find('.dashboard-slug') + slugs = revisions.find(".dashboard-slug") assert slugs.length == REVS_PER_USER for slug in slugs: - assert slug.text_content() == 'wiki_user_2-doc' + assert slug.text_content() == "wiki_user_2-doc" -@pytest.mark.parametrize('authors', (RevisionDashboardForm.KNOWN_AUTHORS, - RevisionDashboardForm.UNKNOWN_AUTHORS, - RevisionDashboardForm.ALL_AUTHORS)) -def test_revisions_known_authors_filter(authors, dashboard_revisions, - user_client, known_author): +@pytest.mark.parametrize( + "authors", + ( + RevisionDashboardForm.KNOWN_AUTHORS, + RevisionDashboardForm.UNKNOWN_AUTHORS, + RevisionDashboardForm.ALL_AUTHORS, + ), +) +def test_revisions_known_authors_filter( + authors, dashboard_revisions, user_client, known_author +): """Revisions can be filtered by the Known Authors group.""" - url = urlparams(reverse('dashboards.revisions'), authors=authors) - response = user_client.get(url, HTTP_HOST=settings.WIKI_HOST, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') + url = urlparams(reverse("dashboards.revisions"), authors=authors) + response = user_client.get( + url, HTTP_HOST=settings.WIKI_HOST, HTTP_X_REQUESTED_WITH="XMLHttpRequest" + ) assert response.status_code == 200 page = pq(response.content) - revisions = page.find('.dashboard-row') + revisions = page.find(".dashboard-row") counts = { RevisionDashboardForm.KNOWN_AUTHORS: REVS_PER_USER, RevisionDashboardForm.UNKNOWN_AUTHORS: 2 * REVS_PER_USER, - RevisionDashboardForm.ALL_AUTHORS: 3 * REVS_PER_USER} + RevisionDashboardForm.ALL_AUTHORS: 3 * REVS_PER_USER, + } expected_count = counts[authors] assert revisions.length == expected_count - author_spans = revisions.find('.dashboard-author') + author_spans = revisions.find(".dashboard-author") assert author_spans.length == expected_count if authors != RevisionDashboardForm.ALL_AUTHORS: for author_span in author_spans: @@ -224,90 +255,106 @@ def test_revisions_known_authors_filter(authors, dashboard_revisions, def test_revisions_creator_overrides_known_authors_filter( - dashboard_revisions, user_client, known_author): + dashboard_revisions, user_client, known_author +): """If the creator filter is set, the Known Authors filter is ignored.""" - url = urlparams(reverse('dashboards.revisions'), - user='wiki_user_3', - authors=RevisionDashboardForm.KNOWN_AUTHORS) - response = user_client.get(url, HTTP_HOST=settings.WIKI_HOST, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') + url = urlparams( + reverse("dashboards.revisions"), + user="wiki_user_3", + authors=RevisionDashboardForm.KNOWN_AUTHORS, + ) + response = user_client.get( + url, HTTP_HOST=settings.WIKI_HOST, HTTP_X_REQUESTED_WITH="XMLHttpRequest" + ) assert response.status_code == 200 page = pq(response.content) - revisions = page.find('.dashboard-row') + revisions = page.find(".dashboard-row") assert revisions.length == REVS_PER_USER - author_spans = revisions.find('.dashboard-author') + author_spans = revisions.find(".dashboard-author") assert author_spans.length == REVS_PER_USER for author_span in author_spans: username = author_span.text_content().strip() - assert username == 'wiki_user_3' + assert username == "wiki_user_3" -def test_revisions_deleted_document(dashboard_revisions, user_client, - wiki_user): +def test_revisions_deleted_document(dashboard_revisions, user_client, wiki_user): """The revisions dashboard includes deleted documents.""" del_doc = dashboard_revisions[0].document DocumentDeletionLog.objects.create( - slug=del_doc.slug, locale=del_doc.locale, user=wiki_user, - reason='Testing deleted docs.') + slug=del_doc.slug, + locale=del_doc.locale, + user=wiki_user, + reason="Testing deleted docs.", + ) del_doc.delete() - response = user_client.get(reverse('dashboards.revisions'), - HTTP_HOST=settings.WIKI_HOST) + response = user_client.get( + reverse("dashboards.revisions"), HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 200 page = pq(response.content) - rev_rows = page.find('.dashboard-row') + rev_rows = page.find(".dashboard-row") assert rev_rows.length == len(dashboard_revisions) == 3 * REVS_PER_USER # Deleted document has a "deleted" tag - assert pq(rev_rows[0]).find('span.deleted') - assert not pq(rev_rows[1]).find('span.deleted') + assert pq(rev_rows[0]).find("span.deleted") + assert not pq(rev_rows[1]).find("span.deleted") -@mock.patch('kuma.dashboards.utils.analytics_upageviews') +@mock.patch("kuma.dashboards.utils.analytics_upageviews") class SpamDashTest(SampleRevisionsMixin, UserTestCase): - fixtures = UserTestCase.fixtures + ['wiki/documents.json'] + fixtures = UserTestCase.fixtures + ["wiki/documents.json"] def test_not_logged_in(self, mock_analytics_upageviews): """A user who is not logged in is not able to see the dashboard.""" - response = self.client.get(reverse('dashboards.spam'), - HTTP_HOST=settings.WIKI_HOST) + response = self.client.get( + reverse("dashboards.spam"), HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 302 assert_no_cache_header(response) def test_permissions(self, mock_analytics_upageviews): """A user with correct permissions is able to see the dashboard.""" - self.client.login(username='testuser', password='testpass') + self.client.login(username="testuser", password="testpass") # Attempt to see spam dashboard as a logged-in user without permissions - response = self.client.get(reverse('dashboards.spam'), - HTTP_HOST=settings.WIKI_HOST) + response = self.client.get( + reverse("dashboards.spam"), HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 403 # Give testuser wiki.add_revisionakismetsubmission permission - perm_akismet = Permission.objects.get(codename='add_revisionakismetsubmission') + perm_akismet = Permission.objects.get(codename="add_revisionakismetsubmission") self.testuser.user_permissions.add(perm_akismet) - response = self.client.get(reverse('dashboards.spam'), - HTTP_HOST=settings.WIKI_HOST) + response = self.client.get( + reverse("dashboards.spam"), HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 403 # Give testuser wiki.add_documentspamattempt permission - perm_spam = Permission.objects.get(codename='add_documentspamattempt') + perm_spam = Permission.objects.get(codename="add_documentspamattempt") self.testuser.user_permissions.add(perm_spam) - response = self.client.get(reverse('dashboards.spam'), - HTTP_HOST=settings.WIKI_HOST) + response = self.client.get( + reverse("dashboards.spam"), HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 403 # Give testuser wiki.add_userban permission - perm_ban = Permission.objects.get(codename='add_userban') + perm_ban = Permission.objects.get(codename="add_userban") self.testuser.user_permissions.add(perm_ban) - response = self.client.get(reverse('dashboards.spam'), - HTTP_HOST=settings.WIKI_HOST) + response = self.client.get( + reverse("dashboards.spam"), HTTP_HOST=settings.WIKI_HOST + ) # With all correct permissions testuser is able to see the dashboard assert response.status_code == 200 assert_no_cache_header(response) - assert 'text/html' in response['Content-Type'] - assert 'dashboards/spam.html' in (template.name for template in response.templates) + assert "text/html" in response["Content-Type"] + assert "dashboards/spam.html" in ( + template.name for template in response.templates + ) - def test_misconfigured_google_analytics_does_not_block(self, mock_analytics_upageviews): + def test_misconfigured_google_analytics_does_not_block( + self, mock_analytics_upageviews + ): """If the constance setting for the Google Analytics API credentials is not configured, or is misconfigured, calls to analytics_upageviews will raise an ImproperlyConfigured error. Show that we still get the rest of @@ -317,29 +364,28 @@ def test_misconfigured_google_analytics_does_not_block(self, mock_analytics_upag mock_analytics_upageviews.side_effect = ImproperlyConfigured("Oops!") rev = self.create_revisions( - num=1, - creator=self.testuser, - document=self.document + num=1, creator=self.testuser, document=self.document )[0] rev.created = datetime.datetime.today() - datetime.timedelta(days=1) rev.save() - rev.akismet_submissions.create(sender=self.admin, type='spam') + rev.akismet_submissions.create(sender=self.admin, type="spam") - self.client.login(username='admin', password='testpass') + self.client.login(username="admin", password="testpass") # The first response will say that the report is being processed - response = self.client.get(reverse('dashboards.spam'), - HTTP_HOST=settings.WIKI_HOST) - self.assertContains( - response, "The report is being processed", status_code=200) + response = self.client.get( + reverse("dashboards.spam"), HTTP_HOST=settings.WIKI_HOST + ) + self.assertContains(response, "The report is being processed", status_code=200) - response2 = self.client.get(reverse('dashboards.spam'), - HTTP_HOST=settings.WIKI_HOST) + response2 = self.client.get( + reverse("dashboards.spam"), HTTP_HOST=settings.WIKI_HOST + ) self.assertContains(response2, "Oops!", status_code=200) page = pq(response2.content) - spam_trends_table = page.find('.spam-trends-table') + spam_trends_table = page.find(".spam-trends-table") assert 1 == len(spam_trends_table) - spam_events_table = page.find('.spam-events-table') + spam_events_table = page.find(".spam-events-table") assert 1 == len(spam_events_table) def test_recent_spam_revisions_show(self, mock_analytics_upageviews): @@ -348,9 +394,7 @@ def test_recent_spam_revisions_show(self, mock_analytics_upageviews): doc2 = create_document(save=True) # Create some revisions by self.testuser rev_doc0 = self.create_revisions( - num=1, - creator=self.testuser, - document=self.document + num=1, creator=self.testuser, document=self.document ) rev_doc1 = self.create_revisions(num=1, creator=self.testuser, document=doc1) rev_doc2 = self.create_revisions(num=1, creator=self.testuser, document=doc2) @@ -368,43 +412,46 @@ def test_recent_spam_revisions_show(self, mock_analytics_upageviews): mock_analytics_upageviews.return_value = { rev_doc0[0].id: 0, rev_doc1[0].id: 0, - rev_doc2[0].id: 0 + rev_doc2[0].id: 0, } - self.client.login(username='admin', password='testpass') + self.client.login(username="admin", password="testpass") # The first response will say that the report is being processed - response = self.client.get(reverse('dashboards.spam'), - HTTP_HOST=settings.WIKI_HOST) + response = self.client.get( + reverse("dashboards.spam"), HTTP_HOST=settings.WIKI_HOST + ) assert 200 == response.status_code - response2 = self.client.get(reverse('dashboards.spam'), - HTTP_HOST=settings.WIKI_HOST) + response2 = self.client.get( + reverse("dashboards.spam"), HTTP_HOST=settings.WIKI_HOST + ) page = pq(response2.content) - table_rows = page.find('.spam-events-table tbody tr') - table_row_text = '' + table_rows = page.find(".spam-events-table tbody tr") + table_row_text = "" for table_row in table_rows: table_row_text += table_row.text_content() assert len(table_rows) == len(created_revisions) for revision in created_revisions: document_url = reverse( - 'wiki.document', - kwargs={'document_path': revision.document.slug} + "wiki.document", kwargs={"document_path": revision.document.slug} ) assert document_url in table_row_text def test_spam_trends_show(self, mock_analytics_upageviews): """The spam trends table shows up.""" - self.client.login(username='admin', password='testpass') + self.client.login(username="admin", password="testpass") # The first response will say that the report is being processed - response = self.client.get(reverse('dashboards.spam'), - HTTP_HOST=settings.WIKI_HOST) + response = self.client.get( + reverse("dashboards.spam"), HTTP_HOST=settings.WIKI_HOST + ) assert 200 == response.status_code - response2 = self.client.get(reverse('dashboards.spam'), - HTTP_HOST=settings.WIKI_HOST) + response2 = self.client.get( + reverse("dashboards.spam"), HTTP_HOST=settings.WIKI_HOST + ) page = pq(response2.content) - spam_trends_table = page.find('.spam-trends-table') + spam_trends_table = page.find(".spam-trends-table") assert 1 == len(spam_trends_table) def test_spam_trends_stats(self, mock_analytics_upageviews): @@ -426,7 +473,9 @@ def test_spam_trends_stats(self, mock_analytics_upageviews): # Revisions made by self.testuser: 3 made today, 3 made 3 days ago, # 3 made 10 days ago, 3 made 35 days ago, 3 made 100 days ago - revs = self.create_revisions(num=15, creator=self.testuser, document=self.document) + revs = self.create_revisions( + num=15, creator=self.testuser, document=self.document + ) for i in range(0, 3): revs[i].created = today for i in range(3, 6): @@ -446,13 +495,21 @@ def test_spam_trends_stats(self, mock_analytics_upageviews): spam_rev_10_days_ago = revs[8] spam_rev_35_days_ago = revs[11] spam_rev_100_days_ago = revs[14] - spam_revs = [spam_rev_today, spam_rev_3_days_ago, spam_rev_10_days_ago, - spam_rev_35_days_ago, spam_rev_100_days_ago] + spam_revs = [ + spam_rev_today, + spam_rev_3_days_ago, + spam_rev_10_days_ago, + spam_rev_35_days_ago, + spam_rev_100_days_ago, + ] # Summary of spam submissions spam_weekly = [spam_rev_3_days_ago] spam_monthly = [spam_rev_3_days_ago, spam_rev_10_days_ago] - spam_quarterly = [spam_rev_3_days_ago, spam_rev_10_days_ago, - spam_rev_35_days_ago] + spam_quarterly = [ + spam_rev_3_days_ago, + spam_rev_10_days_ago, + spam_rev_35_days_ago, + ] # All of the spam_revs were published and then marked as spam for rev in spam_revs: rev.save() @@ -468,10 +525,10 @@ def test_spam_trends_stats(self, mock_analytics_upageviews): for i in range(0, true_blocked_spam_num): document_spam_rev_3_days_ago = DocumentSpamAttempt( user=self.testuser, - title='A spam revision', - slug='spam-revision-slug', + title="A spam revision", + slug="spam-revision-slug", document=self.document, - review=DocumentSpamAttempt.SPAM + review=DocumentSpamAttempt.SPAM, ) document_spam_rev_3_days_ago.save() document_spam_rev_3_days_ago.created = three_days_ago @@ -482,10 +539,10 @@ def test_spam_trends_stats(self, mock_analytics_upageviews): for i in range(0, false_blocked_spam_num): document_ham_rev_3_days_ago = DocumentSpamAttempt( user=self.testuser, - title='Not a spam revision', - slug='ham-revision-slug', + title="Not a spam revision", + slug="ham-revision-slug", document=self.document, - review=DocumentSpamAttempt.HAM + review=DocumentSpamAttempt.HAM, ) document_ham_rev_3_days_ago.save() document_ham_rev_3_days_ago.created = three_days_ago @@ -500,20 +557,46 @@ def test_spam_trends_stats(self, mock_analytics_upageviews): # The mock Google Analytics return values for page views mock_analytics_upageviews.return_value = page_views - self.client.login(username='admin', password='testpass') + self.client.login(username="admin", password="testpass") # The first response will say that the report is being processed - response = self.client.get(reverse('dashboards.spam'), - HTTP_HOST=settings.WIKI_HOST) + response = self.client.get( + reverse("dashboards.spam"), HTTP_HOST=settings.WIKI_HOST + ) assert 200 == response.status_code - response2 = self.client.get(reverse('dashboards.spam'), - HTTP_HOST=settings.WIKI_HOST) + response2 = self.client.get( + reverse("dashboards.spam"), HTTP_HOST=settings.WIKI_HOST + ) page = pq(response2.content) - row_daily = page.find('.spam-trends-table tbody tr')[0].text_content().replace(' ', '').strip('\n').split('\n') - row_weekly = page.find('.spam-trends-table tbody tr')[1].text_content().replace(' ', '').strip('\n').split('\n') - row_monthly = page.find('.spam-trends-table tbody tr')[2].text_content().replace(' ', '').strip('\n').split('\n') - row_quarterly = page.find('.spam-trends-table tbody tr')[3].text_content().replace(' ', '').strip('\n').split('\n') + row_daily = ( + page.find(".spam-trends-table tbody tr")[0] + .text_content() + .replace(" ", "") + .strip("\n") + .split("\n") + ) + row_weekly = ( + page.find(".spam-trends-table tbody tr")[1] + .text_content() + .replace(" ", "") + .strip("\n") + .split("\n") + ) + row_monthly = ( + page.find(".spam-trends-table tbody tr")[2] + .text_content() + .replace(" ", "") + .strip("\n") + .split("\n") + ) + row_quarterly = ( + page.find(".spam-trends-table tbody tr")[3] + .text_content() + .replace(" ", "") + .strip("\n") + .split("\n") + ) # These are the columns in the spam dashboard spam trends table period = 0 @@ -528,15 +611,15 @@ def test_spam_trends_stats(self, mock_analytics_upageviews): true_negative_rate = 9 # The periods are identified as 'Daily', 'Weekly', 'Monthly', 'Quarterly' - assert 'Daily' == row_daily[period] - assert 'Weekly' == row_weekly[period] - assert 'Monthly' == row_monthly[period] - assert 'Quarterly' == row_quarterly[period] + assert "Daily" == row_daily[period] + assert "Weekly" == row_weekly[period] + assert "Monthly" == row_monthly[period] + assert "Quarterly" == row_quarterly[period] # The start dates for each period are correct - assert yesterday.strftime('%Y-%m-%d') == row_daily[start_date] - assert weekly_start_date.strftime('%Y-%m-%d') == row_weekly[start_date] - assert monthly_start_date.strftime('%Y-%m-%d') == row_monthly[start_date] - assert quarterly_start_date.strftime('%Y-%m-%d') == row_quarterly[start_date] + assert yesterday.strftime("%Y-%m-%d") == row_daily[start_date] + assert weekly_start_date.strftime("%Y-%m-%d") == row_weekly[start_date] + assert monthly_start_date.strftime("%Y-%m-%d") == row_monthly[start_date] + assert quarterly_start_date.strftime("%Y-%m-%d") == row_quarterly[start_date] # The page views during the week, month, quarter spam_views_week = page_views[spam_rev_3_days_ago.id] spam_views_month = spam_views_week + page_views[spam_rev_10_days_ago.id] @@ -544,16 +627,18 @@ def test_spam_trends_stats(self, mock_analytics_upageviews): spam_views_quarter = spam_views_month + page_views[spam_rev_35_days_ago.id] spam_views_quarter_exclude_month = page_views[spam_rev_35_days_ago.id] # The percentage change in spam viewers - weekly_spam_change_percent = '{:.1%}'.format( - float(spam_views_week - spam_views_month_exclude_week) / spam_views_month_exclude_week + weekly_spam_change_percent = "{:.1%}".format( + float(spam_views_week - spam_views_month_exclude_week) + / spam_views_month_exclude_week ) - monthly_spam_change_percent = '{:.1%}'.format( - float(spam_views_month - spam_views_quarter_exclude_month) / spam_views_quarter_exclude_month + monthly_spam_change_percent = "{:.1%}".format( + float(spam_views_month - spam_views_quarter_exclude_month) + / spam_views_quarter_exclude_month ) - assert '0.0%' == row_daily[spam_viewers_change_percent] + assert "0.0%" == row_daily[spam_viewers_change_percent] assert weekly_spam_change_percent == row_weekly[spam_viewers_change_percent] assert monthly_spam_change_percent == row_monthly[spam_viewers_change_percent] - assert '0.0%' == row_quarterly[spam_viewers_change_percent] + assert "0.0%" == row_quarterly[spam_viewers_change_percent] # The spam viewers assert 0 == int(row_daily[spam_viewers]) assert spam_views_week == int(row_weekly[spam_viewers]) @@ -561,12 +646,15 @@ def test_spam_trends_stats(self, mock_analytics_upageviews): assert spam_views_quarter == int(row_quarterly[spam_viewers]) # The daily average of spam viewers assert float(row_daily[daily_average_viewers]) == 0.0 - assert (row_weekly[daily_average_viewers] == - '{:.1f}'.format(float(spam_views_week) / days_in_week)) - assert (row_monthly[daily_average_viewers] == - '{:.1f}'.format(float(spam_views_month) / days_in_month)) - assert (row_quarterly[daily_average_viewers] == - '{:.1f}'.format(float(spam_views_quarter) / days_in_quarter)) + assert row_weekly[daily_average_viewers] == "{:.1f}".format( + float(spam_views_week) / days_in_week + ) + assert row_monthly[daily_average_viewers] == "{:.1f}".format( + float(spam_views_month) / days_in_month + ) + assert row_quarterly[daily_average_viewers] == "{:.1f}".format( + float(spam_views_quarter) / days_in_quarter + ) # The published spam: 1 this week, 2 this month, 3 this quarter assert not int(row_daily[published_spam]) assert len(spam_weekly) == int(row_weekly[published_spam]) @@ -583,134 +671,130 @@ def test_spam_trends_stats(self, mock_analytics_upageviews): assert false_blocked_spam_num == int(row_monthly[blocked_ham]) assert false_blocked_spam_num == int(row_quarterly[blocked_ham]) # The true positive rate == blocked_spam / total spam - tpr_weekly = '{:.1%}'.format( + tpr_weekly = "{:.1%}".format( true_blocked_spam_num / float(true_blocked_spam_num + len(spam_weekly)) ) - tpr_monthly = '{:.1%}'.format( + tpr_monthly = "{:.1%}".format( true_blocked_spam_num / float(true_blocked_spam_num + len(spam_monthly)) ) - tpr_quarterly = '{:.1%}'.format( + tpr_quarterly = "{:.1%}".format( true_blocked_spam_num / float(true_blocked_spam_num + len(spam_quarterly)) ) - assert '100.0%' == row_daily[true_positive_rate] + assert "100.0%" == row_daily[true_positive_rate] assert tpr_weekly == row_weekly[true_positive_rate] assert tpr_monthly == row_monthly[true_positive_rate] assert tpr_quarterly == row_quarterly[true_positive_rate] # The true negative rate == published ham / total ham - tnr_weekly = '{:.1%}'.format( + tnr_weekly = "{:.1%}".format( len(ham_weekly) / float(false_blocked_spam_num + len(ham_weekly)) ) - tnr_monthly = '{:.1%}'.format( + tnr_monthly = "{:.1%}".format( len(ham_monthly) / float(false_blocked_spam_num + len(ham_monthly)) ) - tnr_quarterly = '{:.1%}'.format( + tnr_quarterly = "{:.1%}".format( len(ham_quarterly) / float(false_blocked_spam_num + len(ham_quarterly)) ) - assert '100.0%' == row_daily[true_negative_rate] + assert "100.0%" == row_daily[true_negative_rate] assert tnr_weekly == row_weekly[true_negative_rate] assert tnr_monthly == row_monthly[true_negative_rate] assert tnr_quarterly == row_quarterly[true_negative_rate] +@pytest.mark.parametrize("http_method", ["put", "post", "delete", "options", "head"]) @pytest.mark.parametrize( - 'http_method', ['put', 'post', 'delete', 'options', 'head']) -@pytest.mark.parametrize( - 'endpoint', ['revisions', 'user_lookup', 'topic_lookup', 'spam', 'macros']) + "endpoint", ["revisions", "user_lookup", "topic_lookup", "spam", "macros"] +) def test_disallowed_methods(db, client, http_method, endpoint): """HTTP methods other than GET & HEAD are not allowed.""" - url = reverse('dashboards.{}'.format(endpoint)) + url = reverse("dashboards.{}".format(endpoint)) response = getattr(client, http_method)(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 405 - if endpoint in ('spam', 'revisions'): + if endpoint in ("spam", "revisions"): assert_no_cache_header(response) else: assert_shared_cache_header(response) - if endpoint in ('user_lookup', 'topic_lookup'): - assert 'Vary' in response - assert 'X-Requested-With' in response['Vary'] + if endpoint in ("user_lookup", "topic_lookup"): + assert "Vary" in response + assert "X-Requested-With" in response["Vary"] -@pytest.mark.parametrize('endpoint', ['user_lookup', 'topic_lookup']) +@pytest.mark.parametrize("endpoint", ["user_lookup", "topic_lookup"]) def test_lookups_require_login(root_doc, client, endpoint): - qs, headers = '', {'HTTP_HOST': settings.WIKI_HOST} - headers.update(HTTP_X_REQUESTED_WITH='XMLHttpRequest') - if endpoint == 'topic_lookup': - qs = '?topic=root' + qs, headers = "", {"HTTP_HOST": settings.WIKI_HOST} + headers.update(HTTP_X_REQUESTED_WITH="XMLHttpRequest") + if endpoint == "topic_lookup": + qs = "?topic=root" else: - qs = '?user=wiki' - url = reverse('dashboards.{}'.format(endpoint)) + qs + qs = "?user=wiki" + url = reverse("dashboards.{}".format(endpoint)) + qs response = client.get(url, **headers) assert response.status_code == 302 - assert '/signin?next=' in response['Location'] + assert "/signin?next=" in response["Location"] -@pytest.mark.parametrize('mode', ['ajax', 'non-ajax']) -@pytest.mark.parametrize('endpoint', ['user_lookup', 'topic_lookup']) +@pytest.mark.parametrize("mode", ["ajax", "non-ajax"]) +@pytest.mark.parametrize("endpoint", ["user_lookup", "topic_lookup"]) def test_lookup(root_doc, wiki_user_2, wiki_user_3, user_client, mode, endpoint): - qs, headers = '', {'HTTP_HOST': settings.WIKI_HOST} - if mode == 'ajax': - if endpoint == 'topic_lookup': - qs = '?topic=root' - expected_content = [{'label': 'Root'}] + qs, headers = "", {"HTTP_HOST": settings.WIKI_HOST} + if mode == "ajax": + if endpoint == "topic_lookup": + qs = "?topic=root" + expected_content = [{"label": "Root"}] else: - qs = '?user=wiki' - expected_content = [{'label': 'wiki_user'}, - {'label': 'wiki_user_2'}, - {'label': 'wiki_user_3'}] - headers.update(HTTP_X_REQUESTED_WITH='XMLHttpRequest') + qs = "?user=wiki" + expected_content = [ + {"label": "wiki_user"}, + {"label": "wiki_user_2"}, + {"label": "wiki_user_3"}, + ] + headers.update(HTTP_X_REQUESTED_WITH="XMLHttpRequest") else: expected_content = [] - url = reverse('dashboards.{}'.format(endpoint)) + qs + url = reverse("dashboards.{}".format(endpoint)) + qs response = user_client.get(url, **headers) assert response.status_code == 200 - assert 'X-Requested-With' in response['Vary'] + assert "X-Requested-With" in response["Vary"] assert_shared_cache_header(response) - assert response['Content-Type'] == 'application/json; charset=utf-8' + assert response["Content-Type"] == "application/json; charset=utf-8" assert json.loads(response.content) == expected_content -@mock.patch('kuma.dashboards.views.macro_usage') +@mock.patch("kuma.dashboards.views.macro_usage") def test_macros(mock_usage, client, db): """The normal macro page is a three-column table.""" mock_usage.return_value = { - 'A11yRoleQuicklinks': { - 'github_subpath': 'A11yRoleQuicklinks.ejs', - 'count': 100, - 'en_count': 50, + "A11yRoleQuicklinks": { + "github_subpath": "A11yRoleQuicklinks.ejs", + "count": 100, + "en_count": 50, } } - response = client.get(reverse('dashboards.macros'), - HTTP_HOST=settings.WIKI_HOST) + response = client.get(reverse("dashboards.macros"), HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 - assert 'Cookie' in response['Vary'] + assert "Cookie" in response["Vary"] assert_shared_cache_header(response) - assert "Found 1 active macro." in response.content.decode('utf8') + assert "Found 1 active macro." in response.content.decode("utf8") page = pq(response.content) assert len(page("table.macros-table")) == 1 assert len(page("th.stat-header")) == 2 -@mock.patch('kuma.dashboards.views.macro_usage') +@mock.patch("kuma.dashboards.views.macro_usage") def test_macros_no_counts(mock_usage, client, db): """The macro page is a one-column table when counts are unavailable.""" mock_usage.return_value = { - 'A11yRoleQuicklinks': { - 'github_subpath': 'A11yRoleQuicklinks.ejs', - 'count': 0, - 'en_count': 0, + "A11yRoleQuicklinks": { + "github_subpath": "A11yRoleQuicklinks.ejs", + "count": 0, + "en_count": 0, }, - 'CSSRef': { - 'github_subpath': 'CSSRef.ejs', - 'count': 0, - 'en_count': 0, - } + "CSSRef": {"github_subpath": "CSSRef.ejs", "count": 0, "en_count": 0}, } - response = client.get(reverse('dashboards.macros'), - HTTP_HOST=settings.WIKI_HOST) + response = client.get(reverse("dashboards.macros"), HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 - assert "Found 2 active macros." in response.content.decode('utf8') + assert "Found 2 active macros." in response.content.decode("utf8") page = pq(response.content) assert len(page("table.macros-table")) == 1 assert len(page("th.stat-header")) == 0 @@ -718,47 +802,45 @@ def test_macros_no_counts(mock_usage, client, db): def test_index(client, db): """The dashboard index can be loaded.""" - response = client.get(reverse('dashboards.index'), - HTTP_HOST=settings.WIKI_HOST) + response = client.get(reverse("dashboards.index"), HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 content = response.content.decode(response.charset) - assert reverse('dashboards.macros') in content - assert reverse('dashboards.spam') not in content - l10n_url = reverse('wiki.list_with_localization_tag', - kwargs={'tag': 'inprogress'}) + assert reverse("dashboards.macros") in content + assert reverse("dashboards.spam") not in content + l10n_url = reverse("wiki.list_with_localization_tag", kwargs={"tag": "inprogress"}) assert l10n_url not in content def test_index_non_english_sees_translations(client, db): """Non-English users see the in-progress translations dashboard.""" - with override('fr'): - response = client.get(reverse('dashboards.index'), - HTTP_HOST=settings.WIKI_HOST) + with override("fr"): + response = client.get(reverse("dashboards.index"), HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 content = response.content.decode(response.charset) - assert reverse('dashboards.macros') in content - assert reverse('dashboards.spam') not in content - l10n_url = reverse('wiki.list_with_localization_tag', - kwargs={'tag': 'inprogress'}) + assert reverse("dashboards.macros") in content + assert reverse("dashboards.spam") not in content + l10n_url = reverse( + "wiki.list_with_localization_tag", kwargs={"tag": "inprogress"} + ) assert l10n_url in content def test_index_admin_sees_spam_dashboard(admin_client): """A moderator can see the spam dashboard in the list.""" - response = admin_client.get(reverse('dashboards.index'), - HTTP_HOST=settings.WIKI_HOST) + response = admin_client.get( + reverse("dashboards.index"), HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 200 content = response.content.decode(response.charset) - assert reverse('dashboards.macros') in content - assert reverse('dashboards.spam') in content - l10n_url = reverse('wiki.list_with_localization_tag', - kwargs={'tag': 'inprogress'}) + assert reverse("dashboards.macros") in content + assert reverse("dashboards.spam") in content + l10n_url = reverse("wiki.list_with_localization_tag", kwargs={"tag": "inprogress"}) assert l10n_url not in content -@pytest.mark.parametrize('endpoint', ['index', 'revisions', 'spam', 'macros']) +@pytest.mark.parametrize("endpoint", ["index", "revisions", "spam", "macros"]) def test_redirect(client, endpoint): """Redirect to the wiki domain if not already.""" - url = reverse('dashboards.{}'.format(endpoint)) + url = reverse("dashboards.{}".format(endpoint)) response = client.get(url) assert_redirect_to_wiki(response, url) diff --git a/kuma/dashboards/urls.py b/kuma/dashboards/urls.py index 0ba64cebb5f..9b7248f4585 100644 --- a/kuma/dashboards/urls.py +++ b/kuma/dashboards/urls.py @@ -1,5 +1,3 @@ - - from django.conf.urls import url from django.views.generic.base import RedirectView @@ -12,28 +10,19 @@ lang_urlpatterns = [ - url(r'^revisions$', - views.revisions, - name='dashboards.revisions'), - url(r'^user_lookup$', - views.user_lookup, - name='dashboards.user_lookup'), - url(r'^topic_lookup$', - views.topic_lookup, - name='dashboards.topic_lookup'), - url(r'^localization$', + url(r"^revisions$", views.revisions, name="dashboards.revisions"), + url(r"^user_lookup$", views.user_lookup, name="dashboards.user_lookup"), + url(r"^topic_lookup$", views.topic_lookup, name="dashboards.topic_lookup"), + url( + r"^localization$", # Here the "shared_cache_control" decorator is an optimization. It # informs the CDN to cache the redirect for a week, so once this URL # has been requested by a client, all other client requests will be # redirected by the CDN instead of this Django service. - shared_cache_control(s_maxage=WEEK)(RedirectView.as_view( - url='/docs/MDN/Doc_status/Overview', - permanent=True, - ))), - url(r'^spam$', - views.spam, - name='dashboards.spam'), - url(r'^macros$', - views.macros, - name='dashboards.macros'), + shared_cache_control(s_maxage=WEEK)( + RedirectView.as_view(url="/docs/MDN/Doc_status/Overview", permanent=True,) + ), + ), + url(r"^spam$", views.spam, name="dashboards.spam"), + url(r"^macros$", views.macros, name="dashboards.macros"), ] diff --git a/kuma/dashboards/utils.py b/kuma/dashboards/utils.py index 34749aa9c08..a9bd3949ef2 100644 --- a/kuma/dashboards/utils.py +++ b/kuma/dashboards/utils.py @@ -1,31 +1,33 @@ - - import datetime from collections import Counter, defaultdict from dateutil import parser from django.core.exceptions import ImproperlyConfigured -from kuma.wiki.models import (DocumentDeletionLog, DocumentSpamAttempt, - Revision, RevisionAkismetSubmission) +from kuma.wiki.models import ( + DocumentDeletionLog, + DocumentSpamAttempt, + Revision, + RevisionAkismetSubmission, +) from kuma.wiki.utils import analytics_upageviews # Rounded to nearby 7-day period for weekly cycles SPAM_PERIODS = ( - (1, 'period_daily', 'Daily'), - (7, 'period_weekly', 'Weekly'), - (28, 'period_monthly', 'Monthly'), - (91, 'period_quarterly', 'Quarterly'), + (1, "period_daily", "Daily"), + (7, "period_weekly", "Weekly"), + (28, "period_monthly", "Monthly"), + (91, "period_quarterly", "Quarterly"), ) def date_range(start, end): """ Return an iterator providing the dates between `start` and `end`, inclusive. """ - if getattr(start, 'date', None) is not None: + if getattr(start, "date", None) is not None: start = start.date() - if getattr(end, 'date', None) is not None: + if getattr(end, "date", None) is not None: end = end.date() days = (end - start).days + 1 return (start + datetime.timedelta(days=d) for d in range(days)) @@ -33,50 +35,53 @@ def date_range(start, end): def chunker(seq, size): for i in range(0, len(seq), size): - yield seq[i:i + size] + yield seq[i : i + size] def spam_day_stats(day): counts = Counter() next_day = day + datetime.timedelta(days=1) - revs = Revision.objects.filter( - created__range=(day, next_day) - ).only('id').prefetch_related('akismet_submissions') + revs = ( + Revision.objects.filter(created__range=(day, next_day)) + .only("id") + .prefetch_related("akismet_submissions") + ) for rev in revs: - if any(a.type == 'spam' for a in rev.akismet_submissions.all()): - counts['published_spam'] += 1 + if any(a.type == "spam" for a in rev.akismet_submissions.all()): + counts["published_spam"] += 1 else: - counts['published_ham'] += 1 + counts["published_ham"] += 1 needs_review = False blocked_edits = DocumentSpamAttempt.objects.filter( - created__range=(day, next_day)).only('review') + created__range=(day, next_day) + ).only("review") for blocked in blocked_edits: # Is it a false positive? if blocked.review == DocumentSpamAttempt.HAM: - counts['blocked_ham'] += 1 + counts["blocked_ham"] += 1 elif blocked.review == DocumentSpamAttempt.SPAM: - counts['blocked_spam'] += 1 + counts["blocked_spam"] += 1 else: if blocked.review == DocumentSpamAttempt.NEEDS_REVIEW: needs_review = True continue events = { - 'published_spam': 0, - 'published_ham': 0, - 'blocked_spam': 0, - 'blocked_ham': 0, + "published_spam": 0, + "published_ham": 0, + "blocked_spam": 0, + "blocked_ham": 0, } events.update(counts) return { - 'version': 1, - 'generated': datetime.datetime.now().isoformat(), - 'day': day.isoformat(), - 'needs_review': needs_review, - 'events': events + "version": 1, + "generated": datetime.datetime.now().isoformat(), + "day": day.isoformat(), + "needs_review": needs_review, + "events": events, } @@ -91,17 +96,19 @@ def spam_dashboard_historical_stats(periods=None, end_date=None): from .jobs import SpamDayStats, SpamDashboardRecentEvents periods = periods or SPAM_PERIODS - if hasattr(end_date, 'date'): + if hasattr(end_date, "date"): end_date = end_date.date() end_date = end_date or (datetime.date.today() - datetime.timedelta(days=1)) longest = max(days for days, identifier, name in periods) spans = [ - (identifier, - days, - name, - end_date - datetime.timedelta(days=days - 1), # current period begins - end_date) + ( + identifier, + days, + name, + end_date - datetime.timedelta(days=days - 1), # current period begins + end_date, + ) for days, identifier, name in periods ] @@ -114,12 +121,12 @@ def spam_dashboard_historical_stats(periods=None, end_date=None): for day in dates: # Gather daily raw stats raw_events = job.get(day) - day_events = raw_events['events'] + day_events = raw_events["events"] # Regenerate stats if there are change attempts with # needs_review marked and the data is stale. - if raw_events['needs_review']: - generated = parser.parse(raw_events['generated']) + if raw_events["needs_review"]: + generated = parser.parse(raw_events["generated"]) age = datetime.datetime.now() - generated if age.total_seconds() > 300: @@ -134,11 +141,11 @@ def spam_dashboard_historical_stats(periods=None, end_date=None): # Prepare output data data = { - 'version': 1, - 'generated': datetime.datetime.now().isoformat(), - 'end_date': end_date, - 'periods': periods, - 'trends': [], + "version": 1, + "generated": datetime.datetime.now().isoformat(), + "end_date": end_date, + "periods": periods, + "trends": [], } job = SpamDashboardRecentEvents() @@ -152,40 +159,42 @@ def spam_dashboard_historical_stats(periods=None, end_date=None): # Accumulate the spam viewer counts for the previous and # current periods of this length. - period['spam_viewers'] = 0 - period['spam_viewers_previous'] = 0 - for item in events_data.get('recent_spam', ()): - if start <= item['date'] <= end: - period['spam_viewers'] += item.get('viewers', 0) - elif previous_start <= item['date'] <= previous_end: - period['spam_viewers_previous'] += item.get('viewers', 0) - - period['spam_viewers_daily_average'] = period['spam_viewers'] / length - if period['spam_viewers_previous']: - delta = period['spam_viewers'] - period['spam_viewers_previous'] - period['spam_viewers_change'] = delta / period['spam_viewers_previous'] - - spam = period['published_spam'] + period['blocked_spam'] - ham = period['published_ham'] + period['blocked_ham'] + period["spam_viewers"] = 0 + period["spam_viewers_previous"] = 0 + for item in events_data.get("recent_spam", ()): + if start <= item["date"] <= end: + period["spam_viewers"] += item.get("viewers", 0) + elif previous_start <= item["date"] <= previous_end: + period["spam_viewers_previous"] += item.get("viewers", 0) + + period["spam_viewers_daily_average"] = period["spam_viewers"] / length + if period["spam_viewers_previous"]: + delta = period["spam_viewers"] - period["spam_viewers_previous"] + period["spam_viewers_change"] = delta / period["spam_viewers_previous"] + + spam = period["published_spam"] + period["blocked_spam"] + ham = period["published_ham"] + period["blocked_ham"] if spam: - period['true_positive_rate'] = period['blocked_spam'] / spam + period["true_positive_rate"] = period["blocked_spam"] / spam else: - period['true_positive_rate'] = 1.0 + period["true_positive_rate"] = 1.0 if ham: - period['true_negative_rate'] = period['published_ham'] / ham + period["true_negative_rate"] = period["published_ham"] / ham else: - period['true_negative_rate'] = 1.0 - - data['trends'].append({ - 'id': period_id, - 'name': period_name, - 'days': length, - 'start': start.isoformat(), - 'end': end.isoformat(), - 'stats': period, - }) + period["true_negative_rate"] = 1.0 + + data["trends"].append( + { + "id": period_id, + "name": period_name, + "days": length, + "start": start.isoformat(), + "end": end.isoformat(), + "stats": period, + } + ) data.update(events_data) return data @@ -195,8 +204,8 @@ def spam_dashboard_recent_events(start=None, end=None): """Gather data for recent spam events.""" now = datetime.datetime.now() data = { - 'events_generated': str(now), - 'recent_spam': [], + "events_generated": str(now), + "recent_spam": [], } # Define the start and end dates/datetimes. @@ -206,13 +215,13 @@ def spam_dashboard_recent_events(start=None, end=None): start = end - datetime.timedelta(days=183) # Gather recent published spam - recent_spam = RevisionAkismetSubmission.objects.filter( - type='spam', - revision__created__gt=start, - revision__created__lt=end - ).select_related( - 'revision__document' - ).order_by('-id') + recent_spam = ( + RevisionAkismetSubmission.objects.filter( + type="spam", revision__created__gt=start, revision__created__lt=end + ) + .select_related("revision__document") + .order_by("-id") + ) # Document is new; document is a translation change_types = { @@ -229,9 +238,9 @@ def spam_dashboard_recent_events(start=None, end=None): # We only care about the spam rev and the one immediately # following, if there is one. revisions = list( - document.revisions.filter( - id__gte=revision.id - ).only('id', 'created').order_by('id')[:2] + document.revisions.filter(id__gte=revision.id) + .only("id", "created") + .order_by("id")[:2] ) # How long was it active? @@ -240,13 +249,13 @@ def spam_dashboard_recent_events(start=None, end=None): try: entry = DocumentDeletionLog.objects.filter( locale=document.locale, slug=document.slug - ).latest('id') + ).latest("id") time_active_raw = entry.timestamp - revision.created time_active = int(time_active_raw.total_seconds()) except DocumentDeletionLog.DoesNotExist: - time_active = 'Deleted' + time_active = "Deleted" else: - time_active = 'Current' + time_active = "Current" else: next_rev = revisions[1] time_active_raw = next_rev.created - revision.created @@ -255,27 +264,29 @@ def spam_dashboard_recent_events(start=None, end=None): change_type = change_types[bool(revision.previous), bool(document.parent)] # Gather table data - data['recent_spam'].append({ - 'date': revision.created.date(), - 'time_active': time_active, - 'revision_id': revision.id, - 'revision_path': revision.get_absolute_url(), - 'change_type': change_type, - 'document_path': revision.document.get_absolute_url(), - }) + data["recent_spam"].append( + { + "date": revision.created.date(), + "time_active": time_active, + "revision_id": revision.id, + "revision_path": revision.get_absolute_url(), + "change_type": change_type, + "document_path": revision.document.get_absolute_url(), + } + ) # Update the data with the number of viewers from Google Analytics. - for chunk in chunker(data['recent_spam'], 250): - start_date = min(item['date'] for item in chunk) - revs = [item['revision_id'] for item in chunk] + for chunk in chunker(data["recent_spam"], 250): + start_date = min(item["date"] for item in chunk) + revs = [item["revision_id"] for item in chunk] try: views = analytics_upageviews(revs, start_date) except ImproperlyConfigured as e: - data['improperly_configured'] = str(e) + data["improperly_configured"] = str(e) break for item in chunk: - item['viewers'] = views[item['revision_id']] + item["viewers"] = views[item["revision_id"]] return data diff --git a/kuma/dashboards/views.py b/kuma/dashboards/views.py index 1f9a8e6dbe4..6195f1978bf 100644 --- a/kuma/dashboards/views.py +++ b/kuma/dashboards/views.py @@ -1,5 +1,3 @@ - - import datetime import json @@ -15,8 +13,11 @@ from django.views.decorators.http import require_GET from django.views.decorators.vary import vary_on_headers -from kuma.core.decorators import (ensure_wiki_domain, login_required, - shared_cache_control) +from kuma.core.decorators import ( + ensure_wiki_domain, + login_required, + shared_cache_control, +) from kuma.core.utils import paginate from kuma.wiki.kumascript import macro_usage from kuma.wiki.models import Document, Revision @@ -30,7 +31,7 @@ @shared_cache_control def index(request): """Index of dashboards.""" - return render(request, 'dashboards/index.html') + return render(request, "dashboards/index.html") @ensure_wiki_domain @@ -41,9 +42,9 @@ def revisions(request): """Dashboard for reviewing revisions""" filter_form = RevisionDashboardForm(request.GET) - page = request.GET.get('page', 1) + page = request.GET.get("page", 1) - revisions = Revision.objects.order_by('-id').defer('content') + revisions = Revision.objects.order_by("-id").defer("content") query_kwargs = False exclude_kwargs = False @@ -53,9 +54,9 @@ def revisions(request): query_kwargs = {} exclude_kwargs = {} query_kwargs_map = { - 'user': 'creator__username__istartswith', - 'locale': 'document__locale', - 'topic': 'slug__icontains', + "user": "creator__username__istartswith", + "locale": "document__locale", + "topic": "slug__icontains", } # Build up a dict of the filter conditions, if any, then apply @@ -65,33 +66,33 @@ def revisions(request): if filter_arg: query_kwargs[kwarg] = filter_arg - start_date = filter_form.cleaned_data['start_date'] + start_date = filter_form.cleaned_data["start_date"] if start_date: - end_date = (filter_form.cleaned_data['end_date'] or - datetime.datetime.now()) - query_kwargs['created__range'] = [start_date, end_date] + end_date = filter_form.cleaned_data["end_date"] or datetime.datetime.now() + query_kwargs["created__range"] = [start_date, end_date] - preceding_period = filter_form.cleaned_data['preceding_period'] + preceding_period = filter_form.cleaned_data["preceding_period"] if preceding_period: # these are messy but work with timedelta's seconds format, # and keep the form and url arguments human readable - if preceding_period == 'month': + if preceding_period == "month": seconds = 30 * 24 * 60 * 60 - if preceding_period == 'week': + if preceding_period == "week": seconds = 7 * 24 * 60 * 60 - if preceding_period == 'day': + if preceding_period == "day": seconds = 24 * 60 * 60 - if preceding_period == 'hour': + if preceding_period == "hour": seconds = 60 * 60 # use the form date if present, otherwise, offset from now - end_date = (filter_form.cleaned_data['end_date'] or - timezone.now()) + end_date = filter_form.cleaned_data["end_date"] or timezone.now() start_date = end_date - datetime.timedelta(seconds=seconds) - query_kwargs['created__range'] = [start_date, end_date] + query_kwargs["created__range"] = [start_date, end_date] - authors_filter = filter_form.cleaned_data['authors'] - if ((not filter_form.cleaned_data['user']) and - authors_filter not in ['', str(RevisionDashboardForm.ALL_AUTHORS)]): + authors_filter = filter_form.cleaned_data["authors"] + if (not filter_form.cleaned_data["user"]) and authors_filter not in [ + "", + str(RevisionDashboardForm.ALL_AUTHORS), + ]: # Get the 'Known Authors' group. try: @@ -103,49 +104,51 @@ def revisions(request): # 'Known Authors' group, otherwise the filter is # 'Unknown Authors', so exclude the 'Known Authors' group. if authors_filter == str(RevisionDashboardForm.KNOWN_AUTHORS): - query_kwargs['creator__groups__pk'] = group.pk + query_kwargs["creator__groups__pk"] = group.pk else: - exclude_kwargs['creator__groups__pk'] = group.pk + exclude_kwargs["creator__groups__pk"] = group.pk if query_kwargs or exclude_kwargs: revisions = revisions.filter(**query_kwargs).exclude(**exclude_kwargs) # prefetch_related needs to come after all filters have been applied to qs - revisions = revisions.prefetch_related('creator__bans').prefetch_related( - Prefetch('document', queryset=Document.admin_objects.only( - 'deleted', 'locale', 'slug'))) - - show_spam_submission = ( - request.user.is_authenticated and - request.user.has_perm('wiki.add_revisionakismetsubmission')) + revisions = revisions.prefetch_related("creator__bans").prefetch_related( + Prefetch( + "document", + queryset=Document.admin_objects.only("deleted", "locale", "slug"), + ) + ) + + show_spam_submission = request.user.is_authenticated and request.user.has_perm( + "wiki.add_revisionakismetsubmission" + ) if show_spam_submission: - revisions = revisions.prefetch_related('akismet_submissions') + revisions = revisions.prefetch_related("akismet_submissions") revisions = paginate(request, revisions, per_page=PAGE_SIZE) context = { - 'revisions': revisions, - 'page': page, - 'show_ips': ( - waffle.switch_is_active('store_revision_ips') and - request.user.is_superuser + "revisions": revisions, + "page": page, + "show_ips": ( + waffle.switch_is_active("store_revision_ips") and request.user.is_superuser ), - 'show_spam_submission': show_spam_submission, + "show_spam_submission": show_spam_submission, } # Serve the response HTML conditionally upon request type if request.is_ajax(): - template = 'dashboards/includes/revision_dashboard_body.html' + template = "dashboards/includes/revision_dashboard_body.html" else: - template = 'dashboards/revisions.html' - context['form'] = filter_form + template = "dashboards/revisions.html" + context["form"] = filter_form return render(request, template, context) @ensure_wiki_domain @shared_cache_control -@vary_on_headers('X-Requested-With') +@vary_on_headers("X-Requested-With") @require_GET @login_required def user_lookup(request): @@ -153,21 +156,23 @@ def user_lookup(request): userlist = [] if request.is_ajax(): - user = request.GET.get('user', '') + user = request.GET.get("user", "") if user: - matches = (get_user_model().objects - .filter(username__istartswith=user) - .order_by('username')) - for username in matches.values_list('username', flat=True)[:20]: - userlist.append({'label': username}) + matches = ( + get_user_model() + .objects.filter(username__istartswith=user) + .order_by("username") + ) + for username in matches.values_list("username", flat=True)[:20]: + userlist.append({"label": username}) data = json.dumps(userlist) - return HttpResponse(data, content_type='application/json; charset=utf-8') + return HttpResponse(data, content_type="application/json; charset=utf-8") @ensure_wiki_domain @shared_cache_control -@vary_on_headers('X-Requested-With') +@vary_on_headers("X-Requested-With") @require_GET @login_required def topic_lookup(request): @@ -175,26 +180,28 @@ def topic_lookup(request): topiclist = [] if request.is_ajax(): - topic = request.GET.get('topic', '') + topic = request.GET.get("topic", "") if topic: - matches = (Document.objects.filter(slug__icontains=topic) - .order_by('slug')) - for slug in matches.values_list('slug', flat=True)[:20]: - topiclist.append({'label': slug}) + matches = Document.objects.filter(slug__icontains=topic).order_by("slug") + for slug in matches.values_list("slug", flat=True)[:20]: + topiclist.append({"label": slug}) data = json.dumps(topiclist) - return HttpResponse(data, - content_type='application/json; charset=utf-8') + return HttpResponse(data, content_type="application/json; charset=utf-8") @ensure_wiki_domain @never_cache @require_GET @login_required -@permission_required(( - 'wiki.add_revisionakismetsubmission', - 'wiki.add_documentspamattempt', - 'users.add_userban'), raise_exception=True) +@permission_required( + ( + "wiki.add_revisionakismetsubmission", + "wiki.add_documentspamattempt", + "users.add_userban", + ), + raise_exception=True, +) def spam(request): """Dashboard for spam moderators.""" @@ -203,9 +210,9 @@ def spam(request): data = SpamDashboardHistoricalStats().get(yesterday) if not data: - return render(request, 'dashboards/spam.html', {'processing': True}) + return render(request, "dashboards/spam.html", {"processing": True}) - return render(request, 'dashboards/spam.html', data) + return render(request, "dashboards/spam.html", data) @ensure_wiki_domain @@ -214,9 +221,6 @@ def spam(request): def macros(request): """Returns table of active macros and their page counts.""" macros = macro_usage() - total = sum(val['count'] for val in macros.values()) - context = { - 'macros': macros, - 'has_counts': total != 0 - } - return render(request, 'dashboards/macros.html', context) + total = sum(val["count"] for val in macros.values()) + context = {"macros": macros, "has_counts": total != 0} + return render(request, "dashboards/macros.html", context) diff --git a/kuma/feeder/admin.py b/kuma/feeder/admin.py index 7c3bce3e558..a86ff1b636c 100644 --- a/kuma/feeder/admin.py +++ b/kuma/feeder/admin.py @@ -1,5 +1,3 @@ - - from django.contrib import admin from .models import Bundle, Entry, Feed diff --git a/kuma/feeder/apps.py b/kuma/feeder/apps.py index 8329310212f..5e20c2581ae 100644 --- a/kuma/feeder/apps.py +++ b/kuma/feeder/apps.py @@ -1,5 +1,3 @@ - - from django.apps import AppConfig from django.utils.translation import ugettext_lazy as _ @@ -11,15 +9,14 @@ class FeederConfig(AppConfig): The Django App Config class to store information about the feeder app and do startup time things. """ - name = 'kuma.feeder' - verbose_name = _('Feeder') + + name = "kuma.feeder" + verbose_name = _("Feeder") def ready(self): """Configure kuma.feeder after models are loaded.""" # Refresh Hacks Blog: every 10 minutes from kuma.feeder.tasks import update_feeds - app.add_periodic_task( - 60 * 10, - update_feeds.s() - ) + + app.add_periodic_task(60 * 10, update_feeds.s()) diff --git a/kuma/feeder/management/commands/update_feeds.py b/kuma/feeder/management/commands/update_feeds.py index 220ead9565f..dfe428fc715 100644 --- a/kuma/feeder/management/commands/update_feeds.py +++ b/kuma/feeder/management/commands/update_feeds.py @@ -1,5 +1,3 @@ - - import logging import time @@ -13,27 +11,24 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument( - '--force', '-f', - help='Fetch even disabled feeds.', - action='store_true') + "--force", "-f", help="Fetch even disabled feeds.", action="store_true" + ) def handle(self, *args, **options): """ Locked command handler to avoid running this command more than once simultaneously. """ - force = options['force'] - verbosity = int(options['verbosity']) + force = options["force"] + verbosity = int(options["verbosity"]) # Setup logging console = logging.StreamHandler(self.stderr) - level = [logging.WARNING, - logging.INFO, - logging.DEBUG][min(verbosity, 2)] - formatter = logging.Formatter('%(levelname)s: %(message)s') + level = [logging.WARNING, logging.INFO, logging.DEBUG][min(verbosity, 2)] + formatter = logging.Formatter("%(levelname)s: %(message)s") console.setLevel(level) console.setFormatter(formatter) - log = logging.getLogger('kuma.feeder') + log = logging.getLogger("kuma.feeder") log.setLevel(level) log.addHandler(console) @@ -41,10 +36,11 @@ def handle(self, *args, **options): log.info("Starting to fetch updated feeds") start = time.time() if force: - log.info('--force option set: Trying to fetch all known feeds.') + log.info("--force option set: Trying to fetch all known feeds.") # Fetch feeds new_entry_count = update_feeds(force) log.info( f"Finished run in {time.time() - start:.2f} seconds " - f"for {new_entry_count} new entries") + f"for {new_entry_count} new entries" + ) diff --git a/kuma/feeder/migrations/0001_initial.py b/kuma/feeder/migrations/0001_initial.py index 18c829b80d8..ec899ed91bd 100644 --- a/kuma/feeder/migrations/0001_initial.py +++ b/kuma/feeder/migrations/0001_initial.py @@ -1,74 +1,124 @@ - - from django.db import models, migrations class Migration(migrations.Migration): - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Bundle', + name="Bundle", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('shortname', models.SlugField(help_text=b'Short name to find this bundle by.', unique=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "shortname", + models.SlugField( + help_text=b"Short name to find this bundle by.", unique=True + ), + ), ], - options={ - }, + options={}, bases=(models.Model,), ), migrations.CreateModel( - name='Entry', + name="Entry", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('guid', models.CharField(max_length=255)), - ('raw', models.TextField()), - ('visible', models.BooleanField(default=True)), - ('last_published', models.DateTimeField()), - ('created', models.DateTimeField(auto_now_add=True, verbose_name=b'Created On')), - ('updated', models.DateTimeField(auto_now=True, verbose_name=b'Last Modified')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("guid", models.CharField(max_length=255)), + ("raw", models.TextField()), + ("visible", models.BooleanField(default=True)), + ("last_published", models.DateTimeField()), + ( + "created", + models.DateTimeField(auto_now_add=True, verbose_name=b"Created On"), + ), + ( + "updated", + models.DateTimeField(auto_now=True, verbose_name=b"Last Modified"), + ), ], options={ - 'ordering': ['-last_published'], - 'verbose_name_plural': 'Entries', + "ordering": ["-last_published"], + "verbose_name_plural": "Entries", }, bases=(models.Model,), ), migrations.CreateModel( - name='Feed', + name="Feed", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('shortname', models.SlugField(help_text=b'Short name to find this feed by.', unique=True)), - ('title', models.CharField(max_length=140)), - ('url', models.CharField(max_length=2048)), - ('etag', models.CharField(max_length=140)), - ('last_modified', models.DateTimeField()), - ('enabled', models.BooleanField(default=True)), - ('disabled_reason', models.CharField(max_length=2048, blank=True)), - ('keep', models.PositiveIntegerField(default=0, help_text=b'Discard all but this amount of entries. 0 == do not discard.')), - ('created', models.DateTimeField(auto_now_add=True, verbose_name=b'Created On')), - ('updated', models.DateTimeField(auto_now=True, verbose_name=b'Last Modified')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "shortname", + models.SlugField( + help_text=b"Short name to find this feed by.", unique=True + ), + ), + ("title", models.CharField(max_length=140)), + ("url", models.CharField(max_length=2048)), + ("etag", models.CharField(max_length=140)), + ("last_modified", models.DateTimeField()), + ("enabled", models.BooleanField(default=True)), + ("disabled_reason", models.CharField(max_length=2048, blank=True)), + ( + "keep", + models.PositiveIntegerField( + default=0, + help_text=b"Discard all but this amount of entries. 0 == do not discard.", + ), + ), + ( + "created", + models.DateTimeField(auto_now_add=True, verbose_name=b"Created On"), + ), + ( + "updated", + models.DateTimeField(auto_now=True, verbose_name=b"Last Modified"), + ), ], - options={ - }, + options={}, bases=(models.Model,), ), migrations.AddField( - model_name='entry', - name='feed', - field=models.ForeignKey(related_name='entries', to='feeder.Feed', on_delete=models.CASCADE), + model_name="entry", + name="feed", + field=models.ForeignKey( + related_name="entries", to="feeder.Feed", on_delete=models.CASCADE + ), preserve_default=True, ), migrations.AlterUniqueTogether( - name='entry', - unique_together={('feed', 'guid')}, + name="entry", unique_together={("feed", "guid")}, ), migrations.AddField( - model_name='bundle', - name='feeds', - field=models.ManyToManyField(related_name='bundles', to='feeder.Feed', blank=True), + model_name="bundle", + name="feeds", + field=models.ManyToManyField( + related_name="bundles", to="feeder.Feed", blank=True + ), preserve_default=True, ), ] diff --git a/kuma/feeder/migrations/0002_auto_20191023_0405.py b/kuma/feeder/migrations/0002_auto_20191023_0405.py index 2b78695eb2e..fa80a612936 100644 --- a/kuma/feeder/migrations/0002_auto_20191023_0405.py +++ b/kuma/feeder/migrations/0002_auto_20191023_0405.py @@ -7,43 +7,50 @@ class Migration(migrations.Migration): dependencies = [ - ('feeder', '0001_initial'), + ("feeder", "0001_initial"), ] operations = [ migrations.AlterField( - model_name='bundle', - name='shortname', - field=models.SlugField(help_text='Short name to find this bundle by.', unique=True), + model_name="bundle", + name="shortname", + field=models.SlugField( + help_text="Short name to find this bundle by.", unique=True + ), ), migrations.AlterField( - model_name='entry', - name='created', - field=models.DateTimeField(auto_now_add=True, verbose_name='Created On'), + model_name="entry", + name="created", + field=models.DateTimeField(auto_now_add=True, verbose_name="Created On"), ), migrations.AlterField( - model_name='entry', - name='updated', - field=models.DateTimeField(auto_now=True, verbose_name='Last Modified'), + model_name="entry", + name="updated", + field=models.DateTimeField(auto_now=True, verbose_name="Last Modified"), ), migrations.AlterField( - model_name='feed', - name='created', - field=models.DateTimeField(auto_now_add=True, verbose_name='Created On'), + model_name="feed", + name="created", + field=models.DateTimeField(auto_now_add=True, verbose_name="Created On"), ), migrations.AlterField( - model_name='feed', - name='keep', - field=models.PositiveIntegerField(default=0, help_text='Discard all but this amount of entries. 0 == do not discard.'), + model_name="feed", + name="keep", + field=models.PositiveIntegerField( + default=0, + help_text="Discard all but this amount of entries. 0 == do not discard.", + ), ), migrations.AlterField( - model_name='feed', - name='shortname', - field=models.SlugField(help_text='Short name to find this feed by.', unique=True), + model_name="feed", + name="shortname", + field=models.SlugField( + help_text="Short name to find this feed by.", unique=True + ), ), migrations.AlterField( - model_name='feed', - name='updated', - field=models.DateTimeField(auto_now=True, verbose_name='Last Modified'), + model_name="feed", + name="updated", + field=models.DateTimeField(auto_now=True, verbose_name="Last Modified"), ), ] diff --git a/kuma/feeder/models.py b/kuma/feeder/models.py index f4a3194ecae..d32c16dfa43 100644 --- a/kuma/feeder/models.py +++ b/kuma/feeder/models.py @@ -1,5 +1,3 @@ - - import jsonpickle from django.db import models from django.utils.functional import cached_property @@ -17,9 +15,9 @@ class Bundle(models.Model): """A bundle of several feeds. A feed can be in several (or no) bundles.""" shortname = models.SlugField( - help_text='Short name to find this bundle by.', unique=True) - feeds = models.ManyToManyField('feeder.Feed', related_name='bundles', - blank=True) + help_text="Short name to find this bundle by.", unique=True + ) + feeds = models.ManyToManyField("feeder.Feed", related_name="bundles", blank=True) objects = BundleManager() @@ -31,7 +29,8 @@ class Feed(models.Model): """A feed holds the metadata of an RSS feed.""" shortname = models.SlugField( - help_text='Short name to find this feed by.', unique=True) + help_text="Short name to find this feed by.", unique=True + ) title = models.CharField(max_length=140) url = models.CharField(max_length=2048) @@ -45,13 +44,12 @@ class Feed(models.Model): disabled_reason = models.CharField(max_length=2048, blank=True) keep = models.PositiveIntegerField( - default=0, help_text=('Discard all but this amount of entries. 0 == ' - 'do not discard.')) + default=0, + help_text=("Discard all but this amount of entries. 0 == " "do not discard."), + ) - created = models.DateTimeField( - auto_now_add=True, verbose_name='Created On') - updated = models.DateTimeField( - auto_now=True, verbose_name='Last Modified') + created = models.DateTimeField(auto_now_add=True, verbose_name="Created On") + updated = models.DateTimeField(auto_now=True, verbose_name="Last Modified") def __str__(self): return self.shortname @@ -61,7 +59,7 @@ def delete_old_entries(self): if not self.keep > 0: return - to_delete = self.entries.order_by('-last_published')[self.keep:] + to_delete = self.entries.order_by("-last_published")[self.keep :] for item in to_delete: # This doesn't perform extremely well, but it's what we have to do # to keep exactly `n` entries around, as LIMIT is invalid in a @@ -72,7 +70,7 @@ def delete_old_entries(self): class Entry(models.Model): """An entry is an item representing feed content.""" - feed = models.ForeignKey(Feed, related_name='entries', on_delete=models.CASCADE) + feed = models.ForeignKey(Feed, related_name="entries", on_delete=models.CASCADE) guid = models.CharField(max_length=255) raw = models.TextField() @@ -82,18 +80,16 @@ class Entry(models.Model): # Feed entry updated field last_published = models.DateTimeField() - created = models.DateTimeField( - auto_now_add=True, verbose_name='Created On') - updated = models.DateTimeField( - auto_now=True, verbose_name='Last Modified') + created = models.DateTimeField(auto_now_add=True, verbose_name="Created On") + updated = models.DateTimeField(auto_now=True, verbose_name="Last Modified") class Meta: - ordering = ['-last_published'] - unique_together = ('feed', 'guid') - verbose_name_plural = 'Entries' + ordering = ["-last_published"] + unique_together = ("feed", "guid") + verbose_name_plural = "Entries" def __str__(self): - return f'{self.feed.shortname}: {self.guid}' + return f"{self.feed.shortname}: {self.guid}" @cached_property def parsed(self): diff --git a/kuma/feeder/sections.py b/kuma/feeder/sections.py index c0f18d01c6f..7ed1e73e5c8 100644 --- a/kuma/feeder/sections.py +++ b/kuma/feeder/sections.py @@ -1,9 +1,7 @@ - - from django.utils.translation import ugettext_lazy as _ class SECTION_HACKS: - short = 'hacks' - pretty = _('Moz Hacks') - updates = 'updates-moz-hacks' + short = "hacks" + pretty = _("Moz Hacks") + updates = "updates-moz-hacks" diff --git a/kuma/feeder/tasks.py b/kuma/feeder/tasks.py index 6a1b3fd7d82..ae05fcacd27 100644 --- a/kuma/feeder/tasks.py +++ b/kuma/feeder/tasks.py @@ -1,5 +1,3 @@ - - from celery import task from kuma.core.decorators import skip_in_maintenance_mode diff --git a/kuma/feeder/tests/test_models.py b/kuma/feeder/tests/test_models.py index c57877301d5..37310960ecd 100644 --- a/kuma/feeder/tests/test_models.py +++ b/kuma/feeder/tests/test_models.py @@ -1,5 +1,3 @@ - - from datetime import datetime, timedelta from uuid import uuid4 @@ -12,15 +10,13 @@ @pytest.fixture def bundle(db): """A test bundle.""" - return Bundle.objects.create(shortname='test-bundle') + return Bundle.objects.create(shortname="test-bundle") @pytest.fixture def feed(bundle): """A test feed.""" - feed = Feed.objects.create( - shortname='test-feed', - last_modified=datetime.now()) + feed = Feed.objects.create(shortname="test-feed", last_modified=datetime.now()) bundle.feeds.add(feed) return feed @@ -31,30 +27,30 @@ def entries(feed): now = datetime.now() entries = [] for day in range(10, 0, -1): - entries.append(Entry.objects.create( - feed=feed, - guid=uuid4(), - last_published=now - timedelta(days=day), - )) + entries.append( + Entry.objects.create( + feed=feed, guid=uuid4(), last_published=now - timedelta(days=day), + ) + ) return entries def test_bundle_manager_recent_entries(entries): """Bundle.object.recent_entries returns recent entries.""" - recents = Bundle.objects.recent_entries('test-bundle') + recents = Bundle.objects.recent_entries("test-bundle") assert len(recents) == 10 - missing = Bundle.objects.recent_entries('tweets') + missing = Bundle.objects.recent_entries("tweets") assert not missing.exists() def test_bundle_unicode(bundle): """str(Bundle) retuns the shortname.""" - assert str(bundle) == 'test-bundle' + assert str(bundle) == "test-bundle" def test_feed_unicode(feed): """str(Feed) retuns the shortname.""" - assert str(feed) == 'test-feed' + assert str(feed) == "test-feed" def test_feed_delete_old_entries(entries): @@ -68,7 +64,7 @@ def test_feed_delete_old_entries(entries): remaining = list(Entry.objects.all()) assert len(remaining) == feed.keep - expected = list(reversed(entries[-feed.keep:])) + expected = list(reversed(entries[-feed.keep :])) assert remaining == expected @@ -84,10 +80,11 @@ def test_entry_unicode(feed): """str(Entry) returns feed and GUID.""" entry = Entry( feed=feed, - guid='374f4947-e6be-4fdd-9a66-6535dc79a722', - last_published=datetime(2018, 2, 27, 16, 3)) + guid="374f4947-e6be-4fdd-9a66-6535dc79a722", + last_published=datetime(2018, 2, 27, 16, 3), + ) - assert str(entry) == 'test-feed: 374f4947-e6be-4fdd-9a66-6535dc79a722' + assert str(entry) == "test-feed: 374f4947-e6be-4fdd-9a66-6535dc79a722" def test_entry_parsed(feed): @@ -95,11 +92,12 @@ def test_entry_parsed(feed): data = { "title": "Episode #1", "content": "

    Here's the first entry of my monthly series.

    ", - "published": datetime(2014, 2, 7) + "published": datetime(2014, 2, 7), } entry = Entry( feed=feed, - guid='4a2f0033-f987-4c07-a9bf-d2fc960c3c56', + guid="4a2f0033-f987-4c07-a9bf-d2fc960c3c56", last_published=datetime(2014, 2, 7), - raw=jsonpickle.encode(data)) + raw=jsonpickle.encode(data), + ) assert entry.parsed == data diff --git a/kuma/feeder/tests/test_tasks.py b/kuma/feeder/tests/test_tasks.py index f94a8a83f97..4c0402a2015 100644 --- a/kuma/feeder/tests/test_tasks.py +++ b/kuma/feeder/tests/test_tasks.py @@ -1,11 +1,9 @@ - - from unittest import mock from ..tasks import update_feeds -@mock.patch('kuma.feeder.tasks.utils_update_feeds') +@mock.patch("kuma.feeder.tasks.utils_update_feeds") def test_update_feeds(mock_update): """The update_feeds task calls the update_feeds utility function.""" update_feeds() diff --git a/kuma/feeder/tests/test_utils.py b/kuma/feeder/tests/test_utils.py index adf1fd9eba9..0ffe4b5cab7 100644 --- a/kuma/feeder/tests/test_utils.py +++ b/kuma/feeder/tests/test_utils.py @@ -1,5 +1,3 @@ - - import socket from datetime import datetime from time import mktime, struct_time @@ -14,49 +12,57 @@ from ..utils import fetch_feed, save_entry, update_feed, update_feeds # URL of the Hacks blog RSS 2.0 feed -HACKS_URL = 'https://hacks.mozilla.org/feed/' +HACKS_URL = "https://hacks.mozilla.org/feed/" # A truncated result from parsing the Hacks blogs with feedparser HACKS_PARSED = FeedParserDict( # Omited attributes: encoding, headers, namespaces bozo=0, - entries=[FeedParserDict( - # Omited attributes: author_detail, authors, content, guidislink, - # links, summary_detail, tags, title_detail, comments, slash_comments, - # wfw_commentrss - author='Jen Simmons', - id='https://hacks.mozilla.org/?p=31957', - link='https://hacks.mozilla.org/2018/02/its-resilient-css-week/', - published='Mon, 26 Feb 2018 15:05:08 +0000', - published_parsed=struct_time((2018, 2, 26, 15, 5, 8, 0, 57, 0)), - summary='Jen Simmons celebrates resilient CSS', - title='It\u2019s Resilient CSS Week', - ), FeedParserDict( - author='James Hobin', - id='https://hacks.mozilla.org/?p=31946', - link=('https://hacks.mozilla.org/2018/02/making-a-clap-sensing' - '-web-thing/'), - published='Thu, 22 Feb 2018 15:55:45 +0000', - published_parsed=struct_time((2018, 2, 22, 15, 55, 45, 3, 53, 0)), - summary=('The Project Things Gateway exists as a platform to bring' - ' all of your IoT devices together under a unified' - ' umbrella.'), - title='Making a Clap-Sensing Web Thing', - )], + entries=[ + FeedParserDict( + # Omited attributes: author_detail, authors, content, guidislink, + # links, summary_detail, tags, title_detail, comments, slash_comments, + # wfw_commentrss + author="Jen Simmons", + id="https://hacks.mozilla.org/?p=31957", + link="https://hacks.mozilla.org/2018/02/its-resilient-css-week/", + published="Mon, 26 Feb 2018 15:05:08 +0000", + published_parsed=struct_time((2018, 2, 26, 15, 5, 8, 0, 57, 0)), + summary="Jen Simmons celebrates resilient CSS", + title="It\u2019s Resilient CSS Week", + ), + FeedParserDict( + author="James Hobin", + id="https://hacks.mozilla.org/?p=31946", + link=( + "https://hacks.mozilla.org/2018/02/making-a-clap-sensing" "-web-thing/" + ), + published="Thu, 22 Feb 2018 15:55:45 +0000", + published_parsed=struct_time((2018, 2, 22, 15, 55, 45, 3, 53, 0)), + summary=( + "The Project Things Gateway exists as a platform to bring" + " all of your IoT devices together under a unified" + " umbrella." + ), + title="Making a Clap-Sensing Web Thing", + ), + ], etag='W/"1da1fc6a456fd49c32a9291b38ec31ee-gzip"', feed=FeedParserDict( # Omited attributes: generator, generator_detail, language, links, # subtitle_detail, sy_updatefrequency, sy_updateperiod, title_detail, - link='https://hacks.mozilla.org', - subtitle='hacks.mozilla.org', - title='Mozilla Hacks \u2013 the Web developer blog', - updated='Mon, 26 Feb 2018 21:23:38 +0000', - updated_parsed=struct_time((2018, 2, 26, 21, 23, 38, 0, 57, 0))), - href='https://hacks.mozilla.org/feed/', + link="https://hacks.mozilla.org", + subtitle="hacks.mozilla.org", + title="Mozilla Hacks \u2013 the Web developer blog", + updated="Mon, 26 Feb 2018 21:23:38 +0000", + updated_parsed=struct_time((2018, 2, 26, 21, 23, 38, 0, 57, 0)), + ), + href="https://hacks.mozilla.org/feed/", status=200, - updated='Mon, 26 Feb 2018 21:23:38 GMT', + updated="Mon, 26 Feb 2018 21:23:38 GMT", updated_parsed=struct_time((2018, 2, 26, 21, 23, 38, 0, 57, 0)), - version='rss20') + version="rss20", +) def modify_fpd(parsed, **kwargs): @@ -71,16 +77,14 @@ def modify_fpd(parsed, **kwargs): def hacks_feed(db): """A Feed for the Hacks Blog.""" return Feed.objects.create( - shortname='moz-hacks', - url=HACKS_URL, - last_modified=datetime(2018, 2, 25) + shortname="moz-hacks", url=HACKS_URL, last_modified=datetime(2018, 2, 25) ) @pytest.fixture def mocked_parse(): """Return test feedparser data instead of making an HTTP GET.""" - with mock.patch('kuma.feeder.utils.feedparser.parse') as mock_parse: + with mock.patch("kuma.feeder.utils.feedparser.parse") as mock_parse: mock_parse.return_value = HACKS_PARSED yield mock_parse @@ -94,10 +98,10 @@ def test_fetch_feed(hacks_feed, mocked_parse): assert feed.etag == 'W/"1da1fc6a456fd49c32a9291b38ec31ee-gzip"' assert feed.last_modified == datetime(2018, 2, 26, 21, 23, 38) assert feed.enabled - assert feed.disabled_reason == '' + assert feed.disabled_reason == "" -@pytest.mark.parametrize('orig_modified', (None, datetime(2018, 2, 1))) +@pytest.mark.parametrize("orig_modified", (None, datetime(2018, 2, 1))) def test_fetch_feed_sets_modified(hacks_feed, mocked_parse, orig_modified): """A feed's last_modified date is updated.""" hacks_feed.last_modified = orig_modified @@ -118,16 +122,16 @@ def test_fetch_feed_sets_enabled(hacks_feed, mocked_parse): def test_fetch_feed_sets_title(hacks_feed, mocked_parse): """A feed can update the title.""" - hacks_feed.title = 'Old Title' + hacks_feed.title = "Old Title" stream = fetch_feed(hacks_feed) assert stream feed = Feed.objects.get() - assert feed.title == 'Mozilla Hacks \u2013 the Web developer blog' + assert feed.title == "Mozilla Hacks \u2013 the Web developer blog" def test_fetch_feed_redirect(hacks_feed, mocked_parse): """If redirected, the URL is updated but the feed is not processed.""" - new_url = 'https://hacks.example.com/feed' + new_url = "https://hacks.example.com/feed" response = modify_fpd(HACKS_PARSED, status=301, url=new_url) mocked_parse.return_value = response stream = fetch_feed(hacks_feed) @@ -137,13 +141,13 @@ def test_fetch_feed_redirect(hacks_feed, mocked_parse): assert feed.url == new_url -@pytest.mark.parametrize('enabled', ('enabled', 'disabled')) +@pytest.mark.parametrize("enabled", ("enabled", "disabled")) def test_fetch_feed_unchanged(hacks_feed, mocked_parse, enabled): """If the ETag matches, the feed is enabled but not processed.""" hacks_feed.last_modified = datetime(2018, 2, 26, 21, 23, 38) hacks_feed.etag = 'W/"1da1fc6a456fd49c32a9291b38ec31ee-gzip"' - hacks_feed.title = 'Mozilla Hacks \u2013 the Web developer blog' - hacks_feed.enabled = (enabled == 'enabled') + hacks_feed.title = "Mozilla Hacks \u2013 the Web developer blog" + hacks_feed.enabled = enabled == "enabled" hacks_feed.save() # Won't be saved in enabled case mocked_parse.return_value = modify_fpd(HACKS_PARSED, status=304) stream = fetch_feed(hacks_feed) @@ -152,7 +156,7 @@ def test_fetch_feed_unchanged(hacks_feed, mocked_parse, enabled): assert feed.enabled -@pytest.mark.parametrize('status', (404, 410)) +@pytest.mark.parametrize("status", (404, 410)) def test_fetch_feed_missing(hacks_feed, mocked_parse, status): """If the feed is gone, it is disabled.""" mocked_parse.return_value = modify_fpd(HACKS_PARSED, status=status) @@ -161,8 +165,8 @@ def test_fetch_feed_missing(hacks_feed, mocked_parse, status): feed = Feed.objects.get() assert not feed.enabled expected = { - 404: 'This is not a feed or it has been removed!', - 410: 'This feed has been removed!', + 404: "This is not a feed or it has been removed!", + 410: "This feed has been removed!", }[status] assert feed.disabled_reason == expected @@ -171,12 +175,12 @@ def test_fetch_feed_timeout(mocked_parse, hacks_feed, settings): """If a feed times out, it is disabled.""" settings.FEEDER_TIMEOUT = 10 mocked_parse.return_value = FeedParserDict( - bozo=1, - bozo_exception=URLError(reason=socket.timeout('timed out'))) + bozo=1, bozo_exception=URLError(reason=socket.timeout("timed out")) + ) stream = fetch_feed(hacks_feed) assert stream is None feed = Feed.objects.get() - assert feed.etag == '' + assert feed.etag == "" assert not feed.enabled expected_reason = "This feed didn't respond after 10 seconds" assert feed.disabled_reason == expected_reason @@ -185,8 +189,8 @@ def test_fetch_feed_timeout(mocked_parse, hacks_feed, settings): def test_fetch_feed_exception(mocked_parse, hacks_feed): """If a feed encounters an exception, it is disabled.""" mocked_parse.return_value = FeedParserDict( - bozo=1, - bozo_exception=Exception('I am grumpy today.')) + bozo=1, bozo_exception=Exception("I am grumpy today.") + ) stream = fetch_feed(hacks_feed) assert stream is None feed = Feed.objects.get() @@ -206,7 +210,7 @@ def test_fetch_feed_unknown_issue(mocked_parse, hacks_feed): assert feed.disabled_reason == expected_reason -@pytest.mark.parametrize('entry_num', (0, 1)) +@pytest.mark.parametrize("entry_num", (0, 1)) def test_save_entry_new_items(hacks_feed, entry_num): """save_entry saves new entries to the database.""" entry_raw = HACKS_PARSED.entries[entry_num] @@ -220,21 +224,26 @@ def test_save_entry_new_items(hacks_feed, entry_num): def test_save_entry_long_guid(hacks_feed): """A entry with a long GUID (usually an URL) is converted to a hash.""" - long_guid = 'https://example.com/a_%s_long_url' % '_'.join(['very'] * 100) + long_guid = "https://example.com/a_%s_long_url" % "_".join(["very"] * 100) entry_raw = modify_fpd(HACKS_PARSED.entries[0], guid=long_guid) assert save_entry(hacks_feed, entry_raw) entry = Entry.objects.get() - assert entry.guid == '36d5f76416dcdf6beb8a01a937858d5a' + assert entry.guid == "36d5f76416dcdf6beb8a01a937858d5a" def test_save_entry_update_existing(hacks_feed): """save_entry updates an existing entry by GUID.""" entry_raw = HACKS_PARSED.entries[0] - Entry.objects.create(feed=hacks_feed, guid=entry_raw.guid, raw='old', - visible=False, last_published=datetime(2010, 1, 1)) + Entry.objects.create( + feed=hacks_feed, + guid=entry_raw.guid, + raw="old", + visible=False, + last_published=datetime(2010, 1, 1), + ) assert save_entry(hacks_feed, entry_raw) entry = Entry.objects.get() - assert entry.raw != 'old' + assert entry.raw != "old" assert entry.visible assert entry.last_published == datetime(2018, 2, 26, 15, 5, 8) @@ -242,10 +251,13 @@ def test_save_entry_update_existing(hacks_feed): def test_save_entry_no_change(hacks_feed): """save_entry returns False if no changes.""" entry_raw = HACKS_PARSED.entries[0] - Entry.objects.create(feed=hacks_feed, guid=entry_raw.guid, - raw=jsonpickle.encode(entry_raw), - visible=True, - last_published=datetime(2018, 2, 26, 15, 5, 8)) + Entry.objects.create( + feed=hacks_feed, + guid=entry_raw.guid, + raw=jsonpickle.encode(entry_raw), + visible=True, + last_published=datetime(2018, 2, 26, 15, 5, 8), + ) assert not save_entry(hacks_feed, entry_raw) @@ -265,7 +277,7 @@ def test_update_feed_delete_old_entries(hacks_feed, mocked_parse): assert entry.last_published == datetime(2018, 2, 26, 15, 5, 8) # Is latest -@mock.patch('kuma.feeder.utils.fetch_feed') +@mock.patch("kuma.feeder.utils.fetch_feed") def test_update_feed_no_feed_changes(mocked_fetch, hacks_feed): """if feed is stale, no entries are processed.""" mocked_fetch.return_value = None @@ -273,7 +285,7 @@ def test_update_feed_no_feed_changes(mocked_fetch, hacks_feed): assert count == 0 -@mock.patch('kuma.feeder.utils.save_entry') +@mock.patch("kuma.feeder.utils.save_entry") def test_update_feed_no_entry_changes(mocked_save, hacks_feed, mocked_parse): """if entries are stale, count is 0.""" mocked_save.return_value = False @@ -298,11 +310,11 @@ def test_update_feeds_skip_disabled(hacks_feed): assert count == 0 -@mock.patch('kuma.feeder.utils.update_feed') +@mock.patch("kuma.feeder.utils.update_feed") def test_update_feeds_resets_timeout_on_exception(mock_update, hacks_feed): """update_feeds resets the socket timeout even on an exception.""" assert socket.getdefaulttimeout() is None - mock_update.side_effect = Exception('Failure') + mock_update.side_effect = Exception("Failure") with pytest.raises(Exception): update_feeds() assert socket.getdefaulttimeout() is None diff --git a/kuma/feeder/utils.py b/kuma/feeder/utils.py index 6f97025d4e5..07fdc70454d 100644 --- a/kuma/feeder/utils.py +++ b/kuma/feeder/utils.py @@ -1,5 +1,3 @@ - - import logging import socket from datetime import datetime @@ -14,7 +12,7 @@ from .models import Entry, Feed -log = logging.getLogger('kuma.feeder') +log = logging.getLogger("kuma.feeder") def update_feeds(include_disabled=True): @@ -52,8 +50,7 @@ def update_feed(feed): stream = fetch_feed(feed) if stream: - log.debug('Processing %s (%s): %s' % (feed.title, feed.shortname, - feed.url)) + log.debug("Processing %s (%s): %s" % (feed.title, feed.shortname, feed.url)) for entry in stream.entries: if save_entry(feed, entry): new_entry_count += 1 @@ -79,23 +76,27 @@ def fetch_feed(feed): if feed.last_modified is None: feed.last_modified = datetime(1975, 1, 10) - log.debug("feed id=%s feed url=%s etag=%s last_modified=%s" % ( - feed.shortname, feed.url, feed.etag, str(feed.last_modified))) - stream = feedparser.parse(feed.url, etag=feed.etag or '', - modified=feed.last_modified.timetuple()) + log.debug( + "feed id=%s feed url=%s etag=%s last_modified=%s" + % (feed.shortname, feed.url, feed.etag, str(feed.last_modified)) + ) + stream = feedparser.parse( + feed.url, etag=feed.etag or "", modified=feed.last_modified.timetuple() + ) url_status = 500 - if 'status' in stream: + if "status" in stream: url_status = stream.status - elif (stream.bozo and - 'bozo_exception' in stream and - isinstance(stream.bozo_exception, URLError) and - isinstance(stream.bozo_exception.reason, socket.timeout)): + elif ( + stream.bozo + and "bozo_exception" in stream + and isinstance(stream.bozo_exception, URLError) + and isinstance(stream.bozo_exception.reason, socket.timeout) + ): url_status = 408 - if url_status == 301 and ('entries' in stream and - len(stream.entries) > 0): + if url_status == 301 and ("entries" in stream and len(stream.entries) > 0): # TODO: Should the feed be processed this round as well? log.info("Feed has moved from <%s> to <%s>", feed.url, stream.url) feed.url = stream.url @@ -121,43 +122,46 @@ def fetch_feed(feed): elif url_status == 408: feed.enabled = False - feed.disabled_reason = ("This feed didn't respond after %d seconds" % - settings.FEEDER_TIMEOUT) + feed.disabled_reason = ( + "This feed didn't respond after %d seconds" % settings.FEEDER_TIMEOUT + ) dirty_feed = True elif url_status >= 400: feed.enabled = False bozo_msg = "" - if 1 == stream.bozo and 'bozo_exception' in stream.keys(): - log.error('Unable to fetch %s Exception: %s', - feed.url, stream.bozo_exception) + if 1 == stream.bozo and "bozo_exception" in stream.keys(): + log.error( + "Unable to fetch %s Exception: %s", feed.url, stream.bozo_exception + ) bozo_msg = stream.bozo_exception - feed.disabled_reason = ("Error while reading the feed: %s __ %s" % - (url_status, bozo_msg)) + feed.disabled_reason = "Error while reading the feed: %s __ %s" % ( + url_status, + bozo_msg, + ) dirty_feed = True else: # We've got a live one... if not feed.enabled or feed.disabled_reason: # Reset disabled status. feed.enabled = True - feed.disabled_reason = '' + feed.disabled_reason = "" dirty_feed = True has_updates = True - if ('etag' in stream and stream.etag != feed.etag and - stream.etag is not None): + if "etag" in stream and stream.etag != feed.etag and stream.etag is not None: log.info("New etag %s" % stream.etag) feed.etag = stream.etag dirty_feed = True - if 'modified_parsed' in stream: + if "modified_parsed" in stream: stream_mod = datetime.fromtimestamp(mktime(stream.modified_parsed)) if stream_mod != feed.last_modified: log.info("New last_modified %s" % stream_mod) feed.last_modified = stream_mod dirty_feed = True - if 'feed' in stream and 'title' in stream.feed: + if "feed" in stream and "title" in stream.feed: if feed.title != stream.feed.title: feed.title = stream.feed.title dirty_feed = True @@ -174,7 +178,7 @@ def save_entry(feed, entry): """Save a new entry or update an existing one.""" json_entry = jsonpickle.encode(entry) - max_guid_length = Entry._meta.get_field('guid').max_length + max_guid_length = Entry._meta.get_field("guid").max_length if len(entry.guid) <= max_guid_length: entry_guid = entry.guid else: @@ -183,14 +187,17 @@ def save_entry(feed, entry): last_published = datetime.fromtimestamp(mktime(entry.published_parsed)) entry, created = Entry.objects.get_or_create( - feed=feed, guid=entry_guid, - defaults={'raw': json_entry, 'visible': True, - 'last_published': last_published}) + feed=feed, + guid=entry_guid, + defaults={"raw": json_entry, "visible": True, "last_published": last_published}, + ) if not created: # Did the entry change? - changed = (entry.raw != json_entry or - not entry.visible or - entry.last_published != last_published) + changed = ( + entry.raw != json_entry + or not entry.visible + or entry.last_published != last_published + ) if changed: entry.raw = json_entry entry.visible = True diff --git a/kuma/health/tests/test_views.py b/kuma/health/tests/test_views.py index 34baa287323..83ebbfa20c2 100644 --- a/kuma/health/tests/test_views.py +++ b/kuma/health/tests/test_views.py @@ -1,52 +1,52 @@ - - import json from unittest import mock import pytest from django.db import DatabaseError from django.urls import reverse -from elasticsearch.exceptions import (ConnectionError as ES_ConnectionError, - NotFoundError) +from elasticsearch.exceptions import ( + ConnectionError as ES_ConnectionError, + NotFoundError, +) from requests.exceptions import ConnectionError as Requests_ConnectionError from kuma.core.tests import assert_no_cache_header from kuma.users.models import User -@pytest.mark.parametrize('http_method', ['put', 'post', 'delete', 'options']) -@pytest.mark.parametrize('endpoint', ['liveness', 'readiness', 'status']) +@pytest.mark.parametrize("http_method", ["put", "post", "delete", "options"]) +@pytest.mark.parametrize("endpoint", ["liveness", "readiness", "status"]) def test_disallowed_methods(client, http_method, endpoint): """Alternate HTTP methods are not allowed.""" - url = reverse('health.{}'.format(endpoint)) + url = reverse("health.{}".format(endpoint)) response = getattr(client, http_method)(url) assert response.status_code == 405 assert_no_cache_header(response) -@pytest.mark.parametrize('http_method', ['get', 'head']) -@pytest.mark.parametrize('endpoint', ['liveness', 'readiness']) +@pytest.mark.parametrize("http_method", ["get", "head"]) +@pytest.mark.parametrize("endpoint", ["liveness", "readiness"]) def test_liveness_and_readiness(db, client, http_method, endpoint): - url = reverse('health.{}'.format(endpoint)) + url = reverse("health.{}".format(endpoint)) response = getattr(client, http_method)(url) assert response.status_code == 204 assert_no_cache_header(response) -@mock.patch('kuma.wiki.models.Document.objects') +@mock.patch("kuma.wiki.models.Document.objects") def test_readiness_with_db_error(mock_manager, db, client): - mock_manager.filter.side_effect = DatabaseError('fubar') - response = client.get(reverse('health.readiness')) + mock_manager.filter.side_effect = DatabaseError("fubar") + response = client.get(reverse("health.readiness")) assert response.status_code == 503 - assert 'fubar' in response.reason_phrase + assert "fubar" in response.reason_phrase assert_no_cache_header(response) @pytest.fixture def mock_request_revision_hash(): - ks_hash = '8da6b8f41' - with mock.patch('kuma.health.views.request_revision_hash') as func: - func.return_value = mock.Mock(spec_set=['status_code', 'text']) + ks_hash = "8da6b8f41" + with mock.patch("kuma.health.views.request_revision_hash") as func: + func.return_value = mock.Mock(spec_set=["status_code", "text"]) func.return_value.status_code = 200 func.return_value.text = ks_hash yield func @@ -54,47 +54,54 @@ def mock_request_revision_hash(): @pytest.fixture def mock_document_objects_count(): - with mock.patch('kuma.health.views.Document') as model: - model.objects = mock.Mock(spec_set=['count']) + with mock.patch("kuma.health.views.Document") as model: + model.objects = mock.Mock(spec_set=["count"]) model.objects.count.return_value = 100 yield model.objects.count @pytest.fixture def mock_search_count(): - with mock.patch('kuma.health.views.WikiDocumentType.search') as search: - search.return_value = mock.Mock(spec_set=['count']) + with mock.patch("kuma.health.views.WikiDocumentType.search") as search: + search.return_value = mock.Mock(spec_set=["count"]) search.return_value.count.return_value = 90 yield search.return_value.count @pytest.fixture def mock_user_objects_filter(): - usernames = ['test-super', 'test-moderator', 'test-new', 'test-banned', - 'viagra-test-123'] + usernames = [ + "test-super", + "test-moderator", + "test-new", + "test-banned", + "viagra-test-123", + ] users = [] for username in usernames: user = User(username=username) - user.set_password('test-password') + user.set_password("test-password") users.append(user) - with mock.patch('kuma.health.views.User') as model: - model.objects = mock.Mock(spec_set=['only']) - model.objects.only.return_value = mock.Mock(spec_set=['filter']) + with mock.patch("kuma.health.views.User") as model: + model.objects = mock.Mock(spec_set=["only"]) + model.objects.only.return_value = mock.Mock(spec_set=["filter"]) filter_func = model.objects.only.return_value.filter filter_func.return_value = users yield filter_func @pytest.fixture -def mock_status_externals(mock_request_revision_hash, - mock_document_objects_count, - mock_search_count, - mock_user_objects_filter): +def mock_status_externals( + mock_request_revision_hash, + mock_document_objects_count, + mock_search_count, + mock_user_objects_filter, +): yield { - 'kumascript': mock_request_revision_hash, - 'document': mock_document_objects_count, - 'search': mock_search_count, - 'test_users': mock_user_objects_filter, + "kumascript": mock_request_revision_hash, + "document": mock_document_objects_count, + "search": mock_search_count, + "test_users": mock_user_objects_filter, } @@ -102,90 +109,93 @@ def test_status(client, settings, mock_status_externals): """The status JSON reflects the test environment.""" # Normalize to docker development settings dev_settings = { - 'ALLOWED_HOSTS': ['*'], - 'ATTACHMENT_HOST': 'demos:8000', - 'ATTACHMENT_ORIGIN': 'demos:8000', - 'DEBUG': False, - 'INTERACTIVE_EXAMPLES_BASE': 'https://interactive-examples.mdn.mozilla.net', - 'MAINTENANCE_MODE': False, - 'PROTOCOL': 'http://', - 'REVISION_HASH': '3f45719d45f15da73ccc15747c28b80ccc8dfee5', - 'SITE_URL': 'http://mdn.localhost:8000', - 'WIKI_SITE_URL': 'http://wiki.mdn.localhost:8000', - 'STATIC_URL': '/static/', + "ALLOWED_HOSTS": ["*"], + "ATTACHMENT_HOST": "demos:8000", + "ATTACHMENT_ORIGIN": "demos:8000", + "DEBUG": False, + "INTERACTIVE_EXAMPLES_BASE": "https://interactive-examples.mdn.mozilla.net", + "MAINTENANCE_MODE": False, + "PROTOCOL": "http://", + "REVISION_HASH": "3f45719d45f15da73ccc15747c28b80ccc8dfee5", + "SITE_URL": "http://mdn.localhost:8000", + "WIKI_SITE_URL": "http://wiki.mdn.localhost:8000", + "STATIC_URL": "/static/", } for name, value in dev_settings.items(): setattr(settings, name, value) - url = reverse('health.status') + url = reverse("health.status") response = client.get(url) assert response.status_code == 200 assert_no_cache_header(response) - assert response['Content-Type'] == 'application/json' + assert response["Content-Type"] == "application/json" data = json.loads(response.content) - assert sorted(data.keys()) == ['request', 'services', 'settings', - 'version'] - assert data['settings'] == dev_settings - assert data['request'] == { - 'host': 'testserver', - 'is_secure': False, - 'scheme': 'http', - 'url': 'http://testserver/_kuma_status.json', + assert sorted(data.keys()) == ["request", "services", "settings", "version"] + assert data["settings"] == dev_settings + assert data["request"] == { + "host": "testserver", + "is_secure": False, + "scheme": "http", + "url": "http://testserver/_kuma_status.json", } - assert sorted(data['services'].keys()) == ['database', 'kumascript', - 'search', 'test_accounts'] - assert data['services']['database'] == { - 'available': True, - 'populated': True, - 'document_count': 100, + assert sorted(data["services"].keys()) == [ + "database", + "kumascript", + "search", + "test_accounts", + ] + assert data["services"]["database"] == { + "available": True, + "populated": True, + "document_count": 100, } - assert data['services']['kumascript'] == { - 'available': True, - 'revision': '8da6b8f41', + assert data["services"]["kumascript"] == { + "available": True, + "revision": "8da6b8f41", } - assert data['services']['search'] == { - 'available': True, - 'populated': True, - 'count': 90, + assert data["services"]["search"] == { + "available": True, + "populated": True, + "count": 90, } - assert data['services']['test_accounts'] == { - 'available': True, + assert data["services"]["test_accounts"] == { + "available": True, } - assert data['version'] == 1 + assert data["version"] == 1 STATUS_SETTINGS_CASES = { - 'ALLOWED_HOSTS': ['localhost', 'testserver'], - 'ATTACHMENT_HOST': 'attachments.test.moz.works', - 'ATTACHMENT_ORIGIN': 'attachments-origin.test.moz.works', - 'DEBUG': True, - 'INTERACTIVE_EXAMPLES_BASE': 'https://interactive-examples.mdn.moz.works', - 'MAINTENANCE_MODE': True, - 'REVISION_HASH': 'NEW_VALUE', - 'SITE_URL': 'https://mdn.moz.works', - 'STATIC_URL': 'https://cdn.test.moz.works/static/', + "ALLOWED_HOSTS": ["localhost", "testserver"], + "ATTACHMENT_HOST": "attachments.test.moz.works", + "ATTACHMENT_ORIGIN": "attachments-origin.test.moz.works", + "DEBUG": True, + "INTERACTIVE_EXAMPLES_BASE": "https://interactive-examples.mdn.moz.works", + "MAINTENANCE_MODE": True, + "REVISION_HASH": "NEW_VALUE", + "SITE_URL": "https://mdn.moz.works", + "STATIC_URL": "https://cdn.test.moz.works/static/", } -@pytest.mark.parametrize('name,new_value', - STATUS_SETTINGS_CASES.items(), - ids=list(STATUS_SETTINGS_CASES)) -def test_status_settings_change(name, new_value, client, settings, - mock_status_externals): +@pytest.mark.parametrize( + "name,new_value", STATUS_SETTINGS_CASES.items(), ids=list(STATUS_SETTINGS_CASES) +) +def test_status_settings_change( + name, new_value, client, settings, mock_status_externals +): """The status JSON reflects the current Django settings.""" assert getattr(settings, name) != new_value setattr(settings, name, new_value) - url = reverse('health.status') + url = reverse("health.status") response = client.get(url) assert response.status_code == 200 data = json.loads(response.content) - assert data['settings'][name] == new_value + assert data["settings"][name] == new_value -@pytest.mark.parametrize('value', ('https://', 'http://')) -def test_status_settings_protocol(value, client, settings, - mock_status_externals): +@pytest.mark.parametrize("value", ("https://", "http://")) +def test_status_settings_protocol(value, client, settings, mock_status_externals): """ The status JSON reflects the PROTOCOL setting @@ -193,143 +203,142 @@ def test_status_settings_protocol(value, client, settings, good fit for test_status_settings_change """ settings.PROTOCOL = value - url = reverse('health.status') + url = reverse("health.status") response = client.get(url) assert response.status_code == 200 data = json.loads(response.content) - assert data['settings']['PROTOCOL'] == value + assert data["settings"]["PROTOCOL"] == value def test_status_failed_database(client, mock_status_externals): """The status JSON shows if the database is unavailable.""" - mock_status_externals['document'].side_effect = DatabaseError('fubar') - url = reverse('health.status') + mock_status_externals["document"].side_effect = DatabaseError("fubar") + url = reverse("health.status") response = client.get(url) data = json.loads(response.content) - assert data['services']['database'] == { - 'available': False, - 'populated': False, - 'document_count': 0, + assert data["services"]["database"] == { + "available": False, + "populated": False, + "document_count": 0, } def test_status_empty_database(client, mock_status_externals): """The status JSON shows if the database is empty.""" - mock_status_externals['document'].return_value = 0 - url = reverse('health.status') + mock_status_externals["document"].return_value = 0 + url = reverse("health.status") response = client.get(url) data = json.loads(response.content) - assert data['services']['database'] == { - 'available': True, - 'populated': False, - 'document_count': 0, + assert data["services"]["database"] == { + "available": True, + "populated": False, + "document_count": 0, } def test_status_failed_search(client, mock_status_externals): """The status JSON shows if ElasticSearch is unavailable.""" - mock_status_externals['search'].side_effect = ES_ConnectionError('No ES') - url = reverse('health.status') + mock_status_externals["search"].side_effect = ES_ConnectionError("No ES") + url = reverse("health.status") response = client.get(url) data = json.loads(response.content) - assert data['services']['search'] == { - 'available': False, - 'populated': False, - 'count': 0, + assert data["services"]["search"] == { + "available": False, + "populated": False, + "count": 0, } def test_status_missing_index(client, mock_status_externals): """The status JSON shows if the ElasticSearch index is not found.""" - mock_status_externals['search'].side_effect = NotFoundError('No Index') - url = reverse('health.status') + mock_status_externals["search"].side_effect = NotFoundError("No Index") + url = reverse("health.status") response = client.get(url) data = json.loads(response.content) - assert data['services']['search'] == { - 'available': True, - 'populated': False, - 'count': 0, + assert data["services"]["search"] == { + "available": True, + "populated": False, + "count": 0, } def test_status_empty_search(client, mock_status_externals): """The status JSON shows if ElasticSearch is unpopulated.""" - mock_status_externals['search'].return_value = 0 - url = reverse('health.status') + mock_status_externals["search"].return_value = 0 + url = reverse("health.status") response = client.get(url) data = json.loads(response.content) - assert data['services']['search'] == { - 'available': True, - 'populated': False, - 'count': 0, + assert data["services"]["search"] == { + "available": True, + "populated": False, + "count": 0, } def test_status_no_kumascript(client, mock_status_externals): """The status JSON shows if KumaScript is unavailable.""" - mock_status_externals['kumascript'].side_effect = ( - Requests_ConnectionError('Nope')) - url = reverse('health.status') + mock_status_externals["kumascript"].side_effect = Requests_ConnectionError("Nope") + url = reverse("health.status") response = client.get(url) data = json.loads(response.content) - assert data['services']['kumascript'] == { - 'available': False, - 'revision': None, + assert data["services"]["kumascript"] == { + "available": False, + "revision": None, } def test_status_failed_kumascript(client, mock_status_externals): """The status JSON shows if KumaScript returns an error code.""" - mock_status_externals['kumascript'].return_value.status_code = 400 - url = reverse('health.status') + mock_status_externals["kumascript"].return_value.status_code = 400 + url = reverse("health.status") response = client.get(url) data = json.loads(response.content) - assert data['services']['kumascript'] == { - 'available': False, - 'revision': None, + assert data["services"]["kumascript"] == { + "available": False, + "revision": None, } def test_status_test_acccounts_no_database(client, mock_status_externals): """The status JSON shows accounts unavailable if no database.""" - mock_status_externals['test_users'].side_effect = DatabaseError('wat') - url = reverse('health.status') + mock_status_externals["test_users"].side_effect = DatabaseError("wat") + url = reverse("health.status") response = client.get(url) data = json.loads(response.content) - assert data['services']['test_accounts'] == { - 'available': False, + assert data["services"]["test_accounts"] == { + "available": False, } def test_status_test_acccounts_unavailable(client, mock_status_externals): """The status JSON shows if the test accounts are unavailable.""" - mock_status_externals['test_users'].return_value = [] - url = reverse('health.status') + mock_status_externals["test_users"].return_value = [] + url = reverse("health.status") response = client.get(url) data = json.loads(response.content) - assert data['services']['test_accounts'] == { - 'available': False, + assert data["services"]["test_accounts"] == { + "available": False, } def test_status_test_acccounts_one_missing(client, mock_status_externals): """The status JSON shows if there is a missing test account.""" - mock_status_externals['test_users'].return_value.pop() - url = reverse('health.status') + mock_status_externals["test_users"].return_value.pop() + url = reverse("health.status") response = client.get(url) data = json.loads(response.content) - assert data['services']['test_accounts'] == { - 'available': False, + assert data["services"]["test_accounts"] == { + "available": False, } def test_status_test_acccounts_wrong_password(client, mock_status_externals): """The status JSON shows if a test account has the wrong password.""" - user = mock_status_externals['test_users'].return_value[0] - user.set_password('not_the_password') - url = reverse('health.status') + user = mock_status_externals["test_users"].return_value[0] + user.set_password("not_the_password") + url = reverse("health.status") response = client.get(url) data = json.loads(response.content) - assert data['services']['test_accounts'] == { - 'available': False, + assert data["services"]["test_accounts"] == { + "available": False, } diff --git a/kuma/health/urls.py b/kuma/health/urls.py index 4d7a6aa9173..8ff0d0c7e4e 100644 --- a/kuma/health/urls.py +++ b/kuma/health/urls.py @@ -1,21 +1,15 @@ - - from django.conf.urls import url from . import views urlpatterns = [ - url(r'^healthz/?$', - views.liveness, - name='health.liveness'), - url(r'^readiness/?$', - views.readiness, - name='health.readiness'), - url(r'^_kuma_status.json$', - views.status, - name='health.status'), - url(r'^csp-violation-capture$', + url(r"^healthz/?$", views.liveness, name="health.liveness"), + url(r"^readiness/?$", views.readiness, name="health.readiness"), + url(r"^_kuma_status.json$", views.status, name="health.status"), + url( + r"^csp-violation-capture$", views.csp_violation_capture, - name='health.csp_violation_capture'), + name="health.csp_violation_capture", + ), ] diff --git a/kuma/health/views.py b/kuma/health/views.py index 47370511ad7..4d34e651510 100644 --- a/kuma/health/views.py +++ b/kuma/health/views.py @@ -1,5 +1,3 @@ - - import json import logging @@ -9,11 +7,12 @@ from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST, require_safe -from elasticsearch.exceptions import (ConnectionError as ES_ConnectionError, - NotFoundError) +from elasticsearch.exceptions import ( + ConnectionError as ES_ConnectionError, + NotFoundError, +) from raven.contrib.django.models import client -from requests.exceptions import (ConnectionError as Requests_ConnectionError, - ReadTimeout) +from requests.exceptions import ConnectionError as Requests_ConnectionError, ReadTimeout from kuma.users.models import User from kuma.wiki.kumascript import request_revision_hash @@ -50,7 +49,7 @@ def readiness(request): # completes without error. Document.objects.filter(pk=1).exists() except DatabaseError as e: - reason_tmpl = 'service unavailable due to database issue ({!s})' + reason_tmpl = "service unavailable due to database issue ({!s})" status, reason = 503, reason_tmpl.format(e) else: status, reason = 204, None @@ -66,102 +65,100 @@ def status(request): Functional tests can use this to customize the test process. """ data = { - 'version': 1, - 'request': { - 'url': request.build_absolute_uri(''), - 'host': request.get_host(), - 'is_secure': request.is_secure(), - 'scheme': request.scheme, + "version": 1, + "request": { + "url": request.build_absolute_uri(""), + "host": request.get_host(), + "is_secure": request.is_secure(), + "scheme": request.scheme, }, - 'services': { - 'database': {}, - 'kumascript': {}, - 'search': {}, - 'test_accounts': {}, + "services": { + "database": {}, + "kumascript": {}, + "search": {}, + "test_accounts": {}, }, - 'settings': { - 'ALLOWED_HOSTS': settings.ALLOWED_HOSTS, - 'ATTACHMENT_HOST': settings.ATTACHMENT_HOST, - 'ATTACHMENT_ORIGIN': settings.ATTACHMENT_ORIGIN, - 'DEBUG': settings.DEBUG, - 'INTERACTIVE_EXAMPLES_BASE': settings.INTERACTIVE_EXAMPLES_BASE, - 'MAINTENANCE_MODE': settings.MAINTENANCE_MODE, - 'PROTOCOL': settings.PROTOCOL, - 'REVISION_HASH': settings.REVISION_HASH, - 'SITE_URL': settings.SITE_URL, - 'STATIC_URL': settings.STATIC_URL, - 'WIKI_SITE_URL': settings.WIKI_SITE_URL, + "settings": { + "ALLOWED_HOSTS": settings.ALLOWED_HOSTS, + "ATTACHMENT_HOST": settings.ATTACHMENT_HOST, + "ATTACHMENT_ORIGIN": settings.ATTACHMENT_ORIGIN, + "DEBUG": settings.DEBUG, + "INTERACTIVE_EXAMPLES_BASE": settings.INTERACTIVE_EXAMPLES_BASE, + "MAINTENANCE_MODE": settings.MAINTENANCE_MODE, + "PROTOCOL": settings.PROTOCOL, + "REVISION_HASH": settings.REVISION_HASH, + "SITE_URL": settings.SITE_URL, + "STATIC_URL": settings.STATIC_URL, + "WIKI_SITE_URL": settings.WIKI_SITE_URL, }, } # Check that database is reachable, populated - doc_data = { - 'available': True, - 'populated': False, - 'document_count': 0 - } + doc_data = {"available": True, "populated": False, "document_count": 0} try: doc_count = Document.objects.count() except DatabaseError: - doc_data['available'] = False + doc_data["available"] = False else: if doc_count: - doc_data['populated'] = True - doc_data['document_count'] = doc_count - data['services']['database'] = doc_data + doc_data["populated"] = True + doc_data["document_count"] = doc_count + data["services"]["database"] = doc_data # Check that KumaScript is reachable ks_data = { - 'available': True, - 'revision': None, + "available": True, + "revision": None, } try: ks_response = request_revision_hash() except (Requests_ConnectionError, ReadTimeout): ks_response = None if not ks_response or ks_response.status_code != 200: - ks_data['available'] = False + ks_data["available"] = False else: - ks_data['revision'] = ks_response.text - data['services']['kumascript'] = ks_data + ks_data["revision"] = ks_response.text + data["services"]["kumascript"] = ks_data # Check that ElasticSearch is reachable, populated - search_data = { - 'available': True, - 'populated': False, - 'count': 0 - } + search_data = {"available": True, "populated": False, "count": 0} try: search_count = WikiDocumentType.search().count() except ES_ConnectionError: - search_data['available'] = False + search_data["available"] = False except NotFoundError: pass # available but unpopulated (and maybe uncreated) else: if search_count: - search_data['populated'] = True - search_data['count'] = search_count - data['services']['search'] = search_data + search_data["populated"] = True + search_data["count"] = search_count + data["services"]["search"] = search_data # Check if the testing accounts are available - test_account_data = { - 'available': False - } - test_account_names = ['test-super', 'test-moderator', 'test-new', - 'test-banned', 'viagra-test-123'] + test_account_data = {"available": False} + test_account_names = [ + "test-super", + "test-moderator", + "test-new", + "test-banned", + "viagra-test-123", + ] try: - users = list(User.objects.only('id', 'username', 'password') - .filter(username__in=test_account_names)) + users = list( + User.objects.only("id", "username", "password").filter( + username__in=test_account_names + ) + ) except DatabaseError: users = [] if len(users) == len(test_account_names): for user in users: - if not user.check_password('test-password'): + if not user.check_password("test-password"): break else: # All users have the testing password - test_account_data['available'] = True - data['services']['test_accounts'] = test_account_data + test_account_data["available"] = True + data["services"]["test_accounts"] = test_account_data return JsonResponse(data) @@ -180,23 +177,19 @@ def csp_violation_capture(request): return HttpResponse() data = client.get_data_from_request(request) - data.update({ - 'level': logging.INFO, - 'logger': 'CSP', - }) + data.update({"level": logging.INFO, "logger": "CSP"}) try: csp_data = json.loads(request.body) except ValueError: # Cannot decode CSP violation data, ignore - return HttpResponseBadRequest('Invalid CSP Report') + return HttpResponseBadRequest("Invalid CSP Report") try: - blocked_uri = csp_data['csp-report']['blocked-uri'] + blocked_uri = csp_data["csp-report"]["blocked-uri"] except KeyError: # Incomplete CSP report - return HttpResponseBadRequest('Incomplete CSP Report') + return HttpResponseBadRequest("Incomplete CSP Report") - client.captureMessage(message='CSP Violation: {}'.format(blocked_uri), - data=data) + client.captureMessage(message="CSP Violation: {}".format(blocked_uri), data=data) - return HttpResponse('Captured CSP violation, thanks for reporting.') + return HttpResponse("Captured CSP violation, thanks for reporting.") diff --git a/kuma/landing/tests/test_templates.py b/kuma/landing/tests/test_templates.py index 32a4ab676a8..c0a73a04d5e 100644 --- a/kuma/landing/tests/test_templates.py +++ b/kuma/landing/tests/test_templates.py @@ -1,5 +1,3 @@ - - from pyquery import PyQuery as pq from kuma.core.urlresolvers import reverse @@ -8,28 +6,26 @@ def test_google_analytics_disabled(db, settings, client): settings.GOOGLE_ANALYTICS_ACCOUNT = None - response = client.get(reverse('home'), follow=True) + response = client.get(reverse("home"), follow=True) assert 200 == response.status_code assert b"ga('create" not in response.content def test_google_analytics_enabled(db, settings, client): - settings.GOOGLE_ANALYTICS_ACCOUNT = 'UA-99999999-9' - response = client.get(reverse('home'), follow=True) + settings.GOOGLE_ANALYTICS_ACCOUNT = "UA-99999999-9" + response = client.get(reverse("home"), follow=True) assert 200 == response.status_code assert b"ga('create" in response.content def test_default_search_filters(db, settings, client): - group = FilterGroup.objects.create(name='Topic', slug='topic') - for name in ['CSS', 'HTML', 'JavaScript']: - Filter.objects.create(group=group, name=name, slug=name.lower(), - default=True) + group = FilterGroup.objects.create(name="Topic", slug="topic") + for name in ["CSS", "HTML", "JavaScript"]: + Filter.objects.create(group=group, name=name, slug=name.lower(), default=True) - response = client.get(reverse('home'), follow=True, - HTTP_HOST=settings.WIKI_HOST) + response = client.get(reverse("home"), follow=True, HTTP_HOST=settings.WIKI_HOST) page = pq(response.content) - filters = page.find('#home-search-form input[type=hidden]') + filters = page.find("#home-search-form input[type=hidden]") - assert 'topic' == filters.eq(0).attr('name') - assert set(p.val() for p in filters.items()) == {'css', 'html', 'javascript'} + assert "topic" == filters.eq(0).attr("name") + assert set(p.val() for p in filters.items()) == {"css", "html", "javascript"} diff --git a/kuma/landing/tests/test_utils.py b/kuma/landing/tests/test_utils.py index 0741b33d5d9..033e3fbeeb6 100644 --- a/kuma/landing/tests/test_utils.py +++ b/kuma/landing/tests/test_utils.py @@ -1,5 +1,3 @@ - - import pytest from django.conf import settings @@ -8,14 +6,16 @@ @pytest.mark.parametrize( - 'domain, expected', - (('testserver', '/static/img/favicon32-local.png'), - (settings.PRODUCTION_DOMAIN, '/static/img/favicon32.png'), - (settings.STAGING_DOMAIN, '/static/img/favicon32-staging.png'), - )) + "domain, expected", + ( + ("testserver", "/static/img/favicon32-local.png"), + (settings.PRODUCTION_DOMAIN, "/static/img/favicon32.png"), + (settings.STAGING_DOMAIN, "/static/img/favicon32-staging.png"), + ), +) def test_favicon_url(settings, domain, expected): settings.DOMAIN = domain settings.ALLOWED_HOSTS.append(domain) - settings.STATIC_URL = '/static/' + settings.STATIC_URL = "/static/" url = favicon_url() assert url == expected diff --git a/kuma/landing/tests/test_views.py b/kuma/landing/tests/test_views.py index 7ab9344a71c..e632b0cb9f1 100644 --- a/kuma/landing/tests/test_views.py +++ b/kuma/landing/tests/test_views.py @@ -5,8 +5,11 @@ from django.core.cache import cache from ratelimit.exceptions import Ratelimited -from kuma.core.tests import (assert_no_cache_header, assert_redirect_to_wiki, - assert_shared_cache_header) +from kuma.core.tests import ( + assert_no_cache_header, + assert_redirect_to_wiki, + assert_shared_cache_header, +) from kuma.core.urlresolvers import reverse @@ -18,108 +21,107 @@ def cleared_cache(): def test_contribute_json(client, db): - response = client.get(reverse('contribute_json')) + response = client.get(reverse("contribute_json")) assert response.status_code == 200 assert_shared_cache_header(response) - assert response['Content-Type'].startswith('application/json') + assert response["Content-Type"].startswith("application/json") -@pytest.mark.parametrize('case', ('DOMAIN', 'WIKI_HOST')) +@pytest.mark.parametrize("case", ("DOMAIN", "WIKI_HOST")) def test_home(client, db, settings, case): - response = client.get(reverse('home', locale='en-US'), - HTTP_HOST=getattr(settings, case)) + response = client.get( + reverse("home", locale="en-US"), HTTP_HOST=getattr(settings, case) + ) assert response.status_code == 200 assert_shared_cache_header(response) - if case == 'WIKI_HOST': - expected_template = 'landing/homepage.html' + if case == "WIKI_HOST": + expected_template = "landing/homepage.html" else: - expected_template = 'landing/react_homepage.html' + expected_template = "landing/react_homepage.html" assert expected_template in (t.name for t in response.templates) -@mock.patch('kuma.landing.views.render') +@mock.patch("kuma.landing.views.render") def test_home_when_rate_limited(mock_render, client, db): """ Cloudfront CDN's don't cache 429's, but let's test this anyway. """ mock_render.side_effect = Ratelimited() - response = client.get(reverse('home')) + response = client.get(reverse("home")) assert response.status_code == 429 assert_no_cache_header(response) -@pytest.mark.parametrize('mode', ['maintenance', 'normal']) +@pytest.mark.parametrize("mode", ["maintenance", "normal"]) def test_maintenance_mode(db, client, settings, mode): - url = reverse('maintenance_mode') - settings.MAINTENANCE_MODE = (mode == 'maintenance') + url = reverse("maintenance_mode") + settings.MAINTENANCE_MODE = mode == "maintenance" response = client.get(url, HTTP_HOST=settings.WIKI_HOST) if settings.MAINTENANCE_MODE: assert response.status_code == 200 - assert ('landing/maintenance-mode.html' in - [t.name for t in response.templates]) + assert "landing/maintenance-mode.html" in [t.name for t in response.templates] else: assert response.status_code == 302 - assert 'Location' in response - assert urlparse(response['Location']).path == '/en-US/' + assert "Location" in response + assert urlparse(response["Location"]).path == "/en-US/" assert_no_cache_header(response) def test_promote_buttons(client, db): - response = client.get(reverse('promote_buttons'), follow=True) + response = client.get(reverse("promote_buttons"), follow=True) assert response.status_code == 200 assert_shared_cache_header(response) def test_robots_not_allowed(client): """By default, robots.txt shows that robots are not allowed.""" - response = client.get(reverse('robots_txt')) + response = client.get(reverse("robots_txt")) assert response.status_code == 200 assert_shared_cache_header(response) - assert response['Content-Type'] == 'text/plain' + assert response["Content-Type"] == "text/plain" content = response.content - assert b'Sitemap: ' not in content - assert b'Disallow: /\n' in content - assert b'Disallow: /admin/\n' not in content + assert b"Sitemap: " not in content + assert b"Disallow: /\n" in content + assert b"Disallow: /admin/\n" not in content def test_robots_allowed_main_website(client, settings): """On the main website, allow robots with restrictions.""" - host = 'main.mdn.moz.works' + host = "main.mdn.moz.works" settings.ALLOW_ROBOTS_WEB_DOMAINS = [host] settings.ALLOWED_HOSTS.append(host) - response = client.get(reverse('robots_txt'), HTTP_HOST=host) + response = client.get(reverse("robots_txt"), HTTP_HOST=host) assert response.status_code == 200 assert_shared_cache_header(response) - assert response['Content-Type'] == 'text/plain' + assert response["Content-Type"] == "text/plain" content = response.content - assert b'Sitemap: ' in content - assert b'Disallow: /\n' not in content - assert b'Disallow: /admin/\n' in content + assert b"Sitemap: " in content + assert b"Disallow: /\n" not in content + assert b"Disallow: /admin/\n" in content def test_robots_allowed_main_attachment_host(client, settings): """On the main attachment host, allow robots without restrictions.""" - host = 'samples.mdn.moz.works' + host = "samples.mdn.moz.works" settings.ALLOW_ROBOTS_DOMAINS = [host] settings.ALLOWED_HOSTS.append(host) - response = client.get(reverse('robots_txt'), HTTP_HOST=host) + response = client.get(reverse("robots_txt"), HTTP_HOST=host) assert response.status_code == 200 assert_shared_cache_header(response) - assert response['Content-Type'] == 'text/plain' + assert response["Content-Type"] == "text/plain" content = response.content - assert content == b'' + assert content == b"" def test_favicon_ico(client, settings): - settings.STATIC_URL = '/static/' - response = client.get('/favicon.ico') + settings.STATIC_URL = "/static/" + response = client.get("/favicon.ico") assert response.status_code == 302 assert_shared_cache_header(response) - assert response['Location'] == '/static/img/favicon32-local.png' + assert response["Location"] == "/static/img/favicon32-local.png" -@pytest.mark.parametrize( - 'endpoint', ['maintenance_mode', 'promote', 'promote_buttons']) +@pytest.mark.parametrize("endpoint", ["maintenance_mode", "promote", "promote_buttons"]) def test_redirect(client, endpoint): """Redirect to the wiki domain if not already.""" url = reverse(endpoint) diff --git a/kuma/landing/urls.py b/kuma/landing/urls.py index 3dc398813b9..3ed44f53918 100644 --- a/kuma/landing/urls.py +++ b/kuma/landing/urls.py @@ -1,5 +1,3 @@ - - from django.conf.urls import url from kuma.core.decorators import shared_cache_control @@ -11,28 +9,18 @@ lang_urlpatterns = [ - url(r'^$', - views.home, - name='home'), - url(r'^maintenance-mode/?$', - views.maintenance_mode, - name='maintenance_mode'), - url(r'^promote/?$', - views.promote_buttons, - name='promote'), - url(r'^promote/buttons/?$', - views.promote_buttons, - name='promote_buttons'), + url(r"^$", views.home, name="home"), + url(r"^maintenance-mode/?$", views.maintenance_mode, name="maintenance_mode"), + url(r"^promote/?$", views.promote_buttons, name="promote"), + url(r"^promote/buttons/?$", views.promote_buttons, name="promote_buttons"), ] urlpatterns = [ - url(r'^contribute\.json$', - views.contribute_json, - name='contribute_json'), - url(r'^robots.txt$', - views.robots_txt, - name='robots_txt'), - url(r'^favicon.ico$', + url(r"^contribute\.json$", views.contribute_json, name="contribute_json"), + url(r"^robots.txt$", views.robots_txt, name="robots_txt"), + url( + r"^favicon.ico$", shared_cache_control(views.FaviconRedirect.as_view(), s_maxage=MONTH), - name='favicon_ico'), + name="favicon_ico", + ), ] diff --git a/kuma/landing/utils.py b/kuma/landing/utils.py index b2ad755d9a4..000aefdd3ae 100644 --- a/kuma/landing/utils.py +++ b/kuma/landing/utils.py @@ -1,5 +1,3 @@ - - from django.conf import settings from django.contrib.staticfiles.storage import staticfiles_storage @@ -7,9 +5,9 @@ def favicon_url(): """Return the path of the basic favicon.""" if settings.DOMAIN == settings.PRODUCTION_DOMAIN: - suffix = '' + suffix = "" elif settings.DOMAIN == settings.STAGING_DOMAIN: - suffix = '-staging' + suffix = "-staging" else: - suffix = '-local' - return staticfiles_storage.url('img/favicon32%s.png' % suffix) + suffix = "-local" + return staticfiles_storage.url("img/favicon32%s.png" % suffix) diff --git a/kuma/landing/views.py b/kuma/landing/views.py index f5faf0f3851..742911f55e9 100644 --- a/kuma/landing/views.py +++ b/kuma/landing/views.py @@ -1,5 +1,3 @@ - - from django.conf import settings from django.http import HttpResponse from django.shortcuts import redirect, render @@ -18,7 +16,7 @@ @shared_cache_control def contribute_json(request): - return static.serve(request, 'contribute.json', document_root=settings.ROOT) + return static.serve(request, "contribute.json", document_root=settings.ROOT) @shared_cache_control @@ -26,14 +24,13 @@ def home(request): """Home page.""" context = {} # Need for both wiki and react homepage - context['updates'] = list( - Bundle.objects.recent_entries(SECTION_HACKS.updates)[:5]) + context["updates"] = list(Bundle.objects.recent_entries(SECTION_HACKS.updates)[:5]) # The default template name - template_name = 'landing/react_homepage.html' + template_name = "landing/react_homepage.html" if is_wiki(request): - template_name = 'landing/homepage.html' - context['default_filters'] = Filter.objects.default_filters() + template_name = "landing/homepage.html" + context["default_filters"] = Filter.objects.default_filters() return render(request, template_name, context) @@ -41,19 +38,19 @@ def home(request): @never_cache def maintenance_mode(request): if settings.MAINTENANCE_MODE: - return render(request, 'landing/maintenance-mode.html') + return render(request, "landing/maintenance-mode.html") else: - return redirect('home') + return redirect("home") @ensure_wiki_domain @shared_cache_control def promote_buttons(request): """Bug 646192: MDN affiliate buttons""" - return render(request, 'landing/promote_buttons.html') + return render(request, "landing/promote_buttons.html") -ROBOTS_ALLOWED_TXT = '''\ +ROBOTS_ALLOWED_TXT = """\ User-agent: * Sitemap: https://developer.mozilla.org/sitemap.xml @@ -104,13 +101,15 @@ def promote_buttons(request): Disallow: /skins Disallow: /*type=feed Disallow: /*users/ -''' + '\n'.join('Disallow: /{locale}/search'.format(locale=locale) - for locale in settings.ENABLED_LOCALES) +""" + "\n".join( + "Disallow: /{locale}/search".format(locale=locale) + for locale in settings.ENABLED_LOCALES +) -ROBOTS_GO_AWAY_TXT = '''\ +ROBOTS_GO_AWAY_TXT = """\ User-Agent: * Disallow: / -''' +""" @shared_cache_control @@ -123,7 +122,7 @@ def robots_txt(request): robots = ROBOTS_ALLOWED_TXT else: robots = ROBOTS_GO_AWAY_TXT - return HttpResponse(robots, content_type='text/plain') + return HttpResponse(robots, content_type="text/plain") class FaviconRedirect(RedirectView): diff --git a/kuma/payments/apps.py b/kuma/payments/apps.py index 6e71349621c..079fbb0c6d2 100644 --- a/kuma/payments/apps.py +++ b/kuma/payments/apps.py @@ -1,7 +1,5 @@ - - from django.apps import AppConfig class PaymentsConfig(AppConfig): - name = 'kuma.payments' + name = "kuma.payments" diff --git a/kuma/payments/constants.py b/kuma/payments/constants.py index d46df8da586..eb1a95dca6c 100644 --- a/kuma/payments/constants.py +++ b/kuma/payments/constants.py @@ -1,4 +1,2 @@ - - -CONTRIBUTION_BETA_FLAG = 'contrib_beta' -RECURRING_PAYMENT_BETA_FLAG = 'recurring_payment_beta' +CONTRIBUTION_BETA_FLAG = "contrib_beta" +RECURRING_PAYMENT_BETA_FLAG = "recurring_payment_beta" diff --git a/kuma/payments/tests/test_utils.py b/kuma/payments/tests/test_utils.py index 313089f0be3..5a6d2236b80 100644 --- a/kuma/payments/tests/test_utils.py +++ b/kuma/payments/tests/test_utils.py @@ -1,61 +1,55 @@ - - from unittest import mock from kuma.payments.utils import ( cancel_stripe_customer_subscription, - get_stripe_customer_data) + get_stripe_customer_data, +) # Subset of data returned for customer # https://stripe.com/docs/api/customers/retrieve simple_customer_data = { - 'sources': {'data': [{'card': {'last4': '0019'}}]}, - 'subscriptions': {'data': [{ - 'id': 'sub_id', - 'plan': {'amount': 6400}, - }]}, + "sources": {"data": [{"card": {"last4": "0019"}}]}, + "subscriptions": {"data": [{"id": "sub_id", "plan": {"amount": 6400}}]}, } -@mock.patch('stripe.Customer.retrieve', return_value=simple_customer_data) +@mock.patch("stripe.Customer.retrieve", return_value=simple_customer_data) def test_get_stripe_customer_data(mock_retrieve): - '''A customer's subscription can be retrieved.''' - data = get_stripe_customer_data('cust_id') + """A customer's subscription can be retrieved.""" + data = get_stripe_customer_data("cust_id") assert data == { - 'stripe_plan_amount': 64, - 'stripe_card_last4': '0019', - 'active_subscriptions': True + "stripe_plan_amount": 64, + "stripe_card_last4": "0019", + "active_subscriptions": True, } -@mock.patch('stripe.Customer.retrieve', return_value=simple_customer_data) -@mock.patch('stripe.Subscription.retrieve', - return_value=mock.Mock(spec_set=['delete'])) +@mock.patch("stripe.Customer.retrieve", return_value=simple_customer_data) +@mock.patch("stripe.Subscription.retrieve", return_value=mock.Mock(spec_set=["delete"])) def test_cancel_stripe_customer_subscription(mock_sub, mock_cust): - '''A stripe customer's subscriptions can be cancelled.''' - cancel_stripe_customer_subscription('cust_id') + """A stripe customer's subscriptions can be cancelled.""" + cancel_stripe_customer_subscription("cust_id") mock_sub.return_value.delete.assert_called_once_with() -@mock.patch('stripe.Customer.retrieve', - return_value={ - 'subscriptions': {'data': [{'id': 'sub_id1'}, - {'id': 'sub_id2'}, - ]}}) -@mock.patch('stripe.Subscription.retrieve', - side_effect=[mock.Mock(spec_set=['delete']), - mock.Mock(spec_set=['delete'])]) +@mock.patch( + "stripe.Customer.retrieve", + return_value={"subscriptions": {"data": [{"id": "sub_id1"}, {"id": "sub_id2"}]}}, +) +@mock.patch( + "stripe.Subscription.retrieve", + side_effect=[mock.Mock(spec_set=["delete"]), mock.Mock(spec_set=["delete"])], +) def test_cancel_stripe_customer_multiple_subscriptions(mock_sub, mock_cust): - '''A stripe customer's subscriptions can be cancelled.''' - cancel_stripe_customer_subscription('cust_id') + """A stripe customer's subscriptions can be cancelled.""" + cancel_stripe_customer_subscription("cust_id") assert mock_sub.call_count == 2 -@mock.patch('stripe.Customer.retrieve', - return_value={'subscriptions': {'data': []}}) -@mock.patch('stripe.Subscription.retrieve', side_effect=Exception('Oops')) +@mock.patch("stripe.Customer.retrieve", return_value={"subscriptions": {"data": []}}) +@mock.patch("stripe.Subscription.retrieve", side_effect=Exception("Oops")) def test_cancel_stripe_customer_no_subscriptions(mock_sub, mock_cust): - '''A stripe customer with no subscriptions returns True.''' - cancel_stripe_customer_subscription('cust_id') + """A stripe customer with no subscriptions returns True.""" + cancel_stripe_customer_subscription("cust_id") assert not mock_sub.called diff --git a/kuma/payments/tests/test_views.py b/kuma/payments/tests/test_views.py index b99f2c1e768..6dc5e759136 100644 --- a/kuma/payments/tests/test_views.py +++ b/kuma/payments/tests/test_views.py @@ -10,160 +10,194 @@ @pytest.fixture def stripe_user(wiki_user): - wiki_user.stripe_customer_id = 'fakeCustomerID123' + wiki_user.stripe_customer_id = "fakeCustomerID123" wiki_user.save() return wiki_user @pytest.mark.django_db -@mock.patch('kuma.payments.views.enabled', return_value=True) +@mock.patch("kuma.payments.views.enabled", return_value=True) def test_payments_view(mock_enabled, client): """The one-time payment page renders.""" - response = client.get(reverse('payments'), HTTP_HOST=settings.WIKI_HOST) + response = client.get(reverse("payments"), HTTP_HOST=settings.WIKI_HOST) assert_no_cache_header(response) assert response.status_code == 200 @pytest.mark.django_db -@mock.patch('kuma.payments.views.enabled', return_value=True) +@mock.patch("kuma.payments.views.enabled", return_value=True) def test_payment_terms_view(mock_enabled, client): """The payment terms page renders.""" - response = client.get(reverse('payment_terms'), - HTTP_HOST=settings.WIKI_HOST) + response = client.get(reverse("payment_terms"), HTTP_HOST=settings.WIKI_HOST) assert_no_cache_header(response) assert response.status_code == 200 @pytest.mark.django_db -@mock.patch('kuma.payments.views.enabled', return_value=True) -@mock.patch('kuma.payments.views.get_stripe_customer_data', return_value=True) -def test_recurring_payment_management_no_customer_id(enabled_, get, - user_client): +@mock.patch("kuma.payments.views.enabled", return_value=True) +@mock.patch("kuma.payments.views.get_stripe_customer_data", return_value=True) +def test_recurring_payment_management_no_customer_id(enabled_, get, user_client): """The recurring payments page shows there are no active subscriptions.""" - response = user_client.get(reverse('recurring_payment_management'), - HTTP_HOST=settings.WIKI_HOST) + response = user_client.get( + reverse("recurring_payment_management"), HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 200 content = response.content.decode(response.charset) - assert ('
  • """ - result = (kuma.wiki.content - .parse(doc_src) - .filter(SectionTOCFilter).serialize()) + result = kuma.wiki.content.parse(doc_src).filter(SectionTOCFilter).serialize() assert normalize_html(expected) == normalize_html(result) @@ -570,9 +559,7 @@ def test_generate_toc_h2():
  • CSS
  • """ - result = (kuma.wiki.content - .parse(doc_src) - .filter(H2TOCFilter).serialize()) + result = kuma.wiki.content.parse(doc_src).filter(H2TOCFilter).serialize() assert normalize_html(expected) == normalize_html(result) @@ -610,15 +597,13 @@ def test_generate_toc_h3(): """ - result = (kuma.wiki.content - .parse(doc_src) - .filter(H3TOCFilter).serialize()) + result = kuma.wiki.content.parse(doc_src).filter(H3TOCFilter).serialize() assert normalize_html(expected) == normalize_html(result) @pytest.mark.toc def test_bug_925043(): - '''Bug 925043 - Redesign TOC has a bunch of empty tags in markup''' + """Bug 925043 - Redesign TOC has a bunch of empty tags in markup""" doc_src = """

    Mastering print

    print 'Hello World!' @@ -628,9 +613,7 @@ def test_bug_925043(): Masteringprint """ - result = (kuma.wiki.content - .parse(doc_src) - .filter(SectionTOCFilter).serialize()) + result = kuma.wiki.content.parse(doc_src).filter(SectionTOCFilter).serialize() assert normalize_html(expected) == normalize_html(result) @@ -653,16 +636,16 @@ def test_noinclude():
    Przykłady 例 예제 示例
    """ - result = (kuma.wiki.content.filter_out_noinclude(doc_src)) + result = kuma.wiki.content.filter_out_noinclude(doc_src) assert normalize_html(expected) == normalize_html(result) def test_noinclude_empty_content(): """Bug 777475: The noinclude filter and pyquery seems to really dislike empty string as input""" - doc_src = '' + doc_src = "" result = kuma.wiki.content.filter_out_noinclude(doc_src) - assert result == '' + assert result == "" def test_bugize_text_lower(): @@ -672,15 +655,15 @@ def test_bugize_text_lower(): def test_bugize_text_upper(): - bad_upper = 'Fixing Bug #12345 again.' + bad_upper = "Fixing Bug #12345 again." good_upper = 'Fixing Bug 12345 again.' assert bugize_text(bad_upper) == Markup(good_upper) def test_filteriframe(): """The filter drops iframe src that does not match the pattern.""" - slug = 'test-code-embed' - embed_url = 'https://sampleserver/en-US/docs/%s$samples/sample1' % slug + slug = "test-code-embed" + embed_url = "https://sampleserver/en-US/docs/%s$samples/sample1" % slug doc_src = """\

    This is a page. Deal with it.

    @@ -689,19 +672,21 @@ def test_filteriframe():

    test

    - """ % dict(embed_url=embed_url) + """ % dict( + embed_url=embed_url + ) patterns = [ - ('https', 'sampleserver', ''), - ('https', 'testserver', ''), + ("https", "sampleserver", ""), + ("https", "testserver", ""), ] result_src = parse(doc_src).filterIframeHosts(patterns).serialize() page = pq(result_src) - assert page('#if1').attr('src') == embed_url - assert page('#if2').attr('src') == 'https://testserver' - assert page('#if3').attr('src') == '' - assert page('#if4').attr('src') == '' - assert page('#if5').attr('src') == '' + assert page("#if1").attr("src") == embed_url + assert page("#if2").attr("src") == "https://testserver" + assert page("#if3").attr("src") == "" + assert page("#if4").attr("src") == "" + assert page("#if5").attr("src") == "" def test_filteriframe_empty_contents(): @@ -715,163 +700,189 @@ def test_filteriframe_empty_contents(): """ - patterns = [('https', 'sampleserver', '')] + patterns = [("https", "sampleserver", "")] result_src = parse(doc_src).filterIframeHosts(patterns).serialize() assert normalize_html(expected_src) == normalize_html(result_src) FILTERIFRAME_ACCEPTED = { - 'youtube_ssl': ('https://www.youtube.com/embed/' - 'iaNoBlae5Qw/?feature=player_detailpage'), - 'prod': ('https://mdn.mozillademos.org/' - 'en-US/docs/Web/CSS/text-align$samples/alignment?revision=456'), - 'newrelic': 'https://rpm.newrelic.com/public/charts/9PqtkrTkoo5', - 'jsfiddle': 'https://jsfiddle.net/78dg25ax/embedded/js,result/', - 'github.io': ('https://mdn.github.io/webgl-examples/' - 'tutorial/sample6/index.html'), - 'ie_moz_net': ('https://interactive-examples.mdn.mozilla.net/' - 'pages/js/array-push.html'), - 'code_sample': (settings.PROTOCOL + settings.ATTACHMENT_HOST + - '/de/docs/Test$samples/test?revision=678'), - 'interactive': (settings.INTERACTIVE_EXAMPLES_BASE + - '/pages/http/headers.html') + "youtube_ssl": ( + "https://www.youtube.com/embed/" "iaNoBlae5Qw/?feature=player_detailpage" + ), + "prod": ( + "https://mdn.mozillademos.org/" + "en-US/docs/Web/CSS/text-align$samples/alignment?revision=456" + ), + "newrelic": "https://rpm.newrelic.com/public/charts/9PqtkrTkoo5", + "jsfiddle": "https://jsfiddle.net/78dg25ax/embedded/js,result/", + "github.io": ( + "https://mdn.github.io/webgl-examples/" "tutorial/sample6/index.html" + ), + "ie_moz_net": ( + "https://interactive-examples.mdn.mozilla.net/" "pages/js/array-push.html" + ), + "code_sample": ( + settings.PROTOCOL + + settings.ATTACHMENT_HOST + + "/de/docs/Test$samples/test?revision=678" + ), + "interactive": (settings.INTERACTIVE_EXAMPLES_BASE + "/pages/http/headers.html"), } FILTERIFRAME_REJECTED = { - 'alien': 'https://some.alien.site.com', - 'dwalsh_web': 'http://davidwalsh.name', - 'dwalsh_ftp': 'ftp://davidwalsh.name', - 'js': 'javascript:alert(1);', - 'youtube_other': 'https://youtube.com/sembed/', - 'prod_old': ('https://mozillademos.org/' - 'en-US/docs/Web/CSS/text-align$samples/alignment?revision=456'), - 'vagrant': ('https://developer-local.allizom.org/' - 'en-US/docs/Test$samples/sample1?revision=123'), - 'vagrant_2': ('http://developer-local:81/' - 'en-US/docs/Test$samples/sample1?revision=123'), - 'cdn': ('https://developer.cdn.mozilla.net/is/this/valid?'), - 'stage': ('https://stage-files.mdn.moz.works/' - 'fr/docs/Test$samples/sample2?revision=234'), - 'test': 'http://testserver/en-US/docs/Test$samples/test?revision=567', - 'youtube_no_www': ('https://youtube.com/embed/' - 'iaNoBlae5Qw/?feature=player_detailpage'), - 'youtube_http': ('http://www.youtube.com/embed/' - 'iaNoBlae5Qw/?feature=player_detailpage'), - 'youtube_other2': 'https://www.youtube.com/sembed/', - 'jsfiddle_other': 'https://jsfiddle.net/about', + "alien": "https://some.alien.site.com", + "dwalsh_web": "http://davidwalsh.name", + "dwalsh_ftp": "ftp://davidwalsh.name", + "js": "javascript:alert(1);", + "youtube_other": "https://youtube.com/sembed/", + "prod_old": ( + "https://mozillademos.org/" + "en-US/docs/Web/CSS/text-align$samples/alignment?revision=456" + ), + "vagrant": ( + "https://developer-local.allizom.org/" + "en-US/docs/Test$samples/sample1?revision=123" + ), + "vagrant_2": ( + "http://developer-local:81/" "en-US/docs/Test$samples/sample1?revision=123" + ), + "cdn": ("https://developer.cdn.mozilla.net/is/this/valid?"), + "stage": ( + "https://stage-files.mdn.moz.works/" "fr/docs/Test$samples/sample2?revision=234" + ), + "test": "http://testserver/en-US/docs/Test$samples/test?revision=567", + "youtube_no_www": ( + "https://youtube.com/embed/" "iaNoBlae5Qw/?feature=player_detailpage" + ), + "youtube_http": ( + "http://www.youtube.com/embed/" "iaNoBlae5Qw/?feature=player_detailpage" + ), + "youtube_other2": "https://www.youtube.com/sembed/", + "jsfiddle_other": "https://jsfiddle.net/about", } -@pytest.mark.parametrize('url', list(FILTERIFRAME_ACCEPTED.values()), - ids=list(FILTERIFRAME_ACCEPTED)) +@pytest.mark.parametrize( + "url", list(FILTERIFRAME_ACCEPTED.values()), ids=list(FILTERIFRAME_ACCEPTED) +) def test_filteriframe_default_accepted(url, settings): doc_src = '' % url patterns = settings.ALLOWED_IFRAME_PATTERNS result_src = parse(doc_src).filterIframeHosts(patterns).serialize() page = pq(result_src) - assert page('#test').attr('src') == url + assert page("#test").attr("src") == url -@pytest.mark.parametrize('url', list(FILTERIFRAME_REJECTED.values()), - ids=list(FILTERIFRAME_REJECTED)) +@pytest.mark.parametrize( + "url", list(FILTERIFRAME_REJECTED.values()), ids=list(FILTERIFRAME_REJECTED) +) def test_filteriframe_default_rejected(url, settings): doc_src = '' % url patterns = settings.ALLOWED_IFRAME_PATTERNS result_src = parse(doc_src).filterIframeHosts(patterns).serialize() page = pq(result_src) - assert page('#test').attr('src') == '' + assert page("#test").attr("src") == "" BLEACH_INVALID_HREFS = { - 'b64_script1': ('data:text/html;base64,' + - b64encode(b'').decode()), - 'javascript': 'javascript:alert(1)', - 'js_htmlref1': 'javas cript:alert(1)', - 'js_htmlref2': 'javascript:alert(1)', + "b64_script1": ( + "data:text/html;base64," + + b64encode(b'").decode() + ), + "javascript": "javascript:alert(1)", + "js_htmlref1": "javas cript:alert(1)", + "js_htmlref2": "javascript:alert(1)", } BLEACH_VALID_HREFS = { - 'relative': '/docs/ok/test', - 'http': 'http://example.com/docs/ok/test', - 'https': 'https://example.com/docs/ok/test', + "relative": "/docs/ok/test", + "http": "http://example.com/docs/ok/test", + "https": "https://example.com/docs/ok/test", } -@pytest.mark.parametrize('href', list(BLEACH_INVALID_HREFS.values()), - ids=list(BLEACH_INVALID_HREFS)) +@pytest.mark.parametrize( + "href", list(BLEACH_INVALID_HREFS.values()), ids=list(BLEACH_INVALID_HREFS) +) def test_bleach_clean_removes_invalid_hrefs(href): """Bleach removes invalid hrefs.""" html = '

    click me

    ' % href - result = bleach.clean(html, - tags=ALLOWED_TAGS, - attributes=ALLOWED_ATTRIBUTES, - protocols=ALLOWED_PROTOCOLS) - link = pq(result).find('#test') - assert link.attr('href') is None + result = bleach.clean( + html, + tags=ALLOWED_TAGS, + attributes=ALLOWED_ATTRIBUTES, + protocols=ALLOWED_PROTOCOLS, + ) + link = pq(result).find("#test") + assert link.attr("href") is None -@pytest.mark.parametrize('href', list(BLEACH_VALID_HREFS.values()), - ids=list(BLEACH_VALID_HREFS)) +@pytest.mark.parametrize( + "href", list(BLEACH_VALID_HREFS.values()), ids=list(BLEACH_VALID_HREFS) +) def test_bleach_clean_hrefs(href): """Bleach retains valid hrefs.""" html = '

    click me

    ' % href - result = bleach.clean(html, - tags=ALLOWED_TAGS, - attributes=ALLOWED_ATTRIBUTES, - protocols=ALLOWED_PROTOCOLS) - link = pq(result).find('#test') - assert link.attr('href') == href + result = bleach.clean( + html, + tags=ALLOWED_TAGS, + attributes=ALLOWED_ATTRIBUTES, + protocols=ALLOWED_PROTOCOLS, + ) + link = pq(result).find("#test") + assert link.attr("href") == href def test_annotate_links_encoded_utf8(db): """Encoded UTF8 characters in links are decoded.""" - Document.objects.create(locale='fr', slug='CSS/Héritage', - title='Héritée') - html = normalize_html( - '
  • Héritée
  • ') + Document.objects.create(locale="fr", slug="CSS/Héritage", title="Héritée") + html = normalize_html('
  • Héritée
  • ') actual_raw = parse(html).annotateLinks(base_url=AL_BASE_URL).serialize() assert normalize_html(actual_raw) == html -@pytest.mark.parametrize('has_class', ('hasClass', 'noClass')) -@pytest.mark.parametrize('anchor', ('withAnchor', 'noAnchor')) -@pytest.mark.parametrize('full_url', ('fullURL', 'pathOnly')) +@pytest.mark.parametrize("has_class", ("hasClass", "noClass")) +@pytest.mark.parametrize("anchor", ("withAnchor", "noAnchor")) +@pytest.mark.parametrize("full_url", ("fullURL", "pathOnly")) def test_annotate_links_existing_doc(root_doc, anchor, full_url, has_class): """Links to existing docs are unmodified.""" - if full_url == 'fullURL': + if full_url == "fullURL": url = root_doc.get_full_url() assert url.startswith(settings.SITE_URL) else: url = root_doc.get_absolute_url() - if anchor == 'withAnchor': + if anchor == "withAnchor": url += "#anchor" - if has_class == 'hasClass': + if has_class == "hasClass": link_class = ' class="extra"' else: - link_class = '' + link_class = "" html = normalize_html('
  • ' % (link_class, url)) actual_raw = normalize_html( - parse(html).annotateLinks(base_url=settings.SITE_URL).serialize()) + parse(html).annotateLinks(base_url=settings.SITE_URL).serialize() + ) assert actual_raw == html -@pytest.mark.parametrize('has_class', ('hasClass', 'noClass')) -@pytest.mark.parametrize('anchor', ('withAnchor', 'noAnchor')) -@pytest.mark.parametrize('full_url', ('fullURL', 'pathOnly')) +@pytest.mark.parametrize("has_class", ("hasClass", "noClass")) +@pytest.mark.parametrize("anchor", ("withAnchor", "noAnchor")) +@pytest.mark.parametrize("full_url", ("fullURL", "pathOnly")) def test_annotate_links_nonexisting_doc(db, anchor, full_url, has_class): """Links to missing docs get extra attributes.""" - url = 'en-US/docs/Root' - if full_url == 'fullURL': + url = "en-US/docs/Root" + if full_url == "fullURL": url = urljoin(AL_BASE_URL, url) - if anchor == 'withAnchor': + if anchor == "withAnchor": url += "#anchor" - if has_class == 'hasClass': + if has_class == "hasClass": link_class = ' class="extra"' expected_attrs = ' class="extra new" rel="nofollow"' else: - link_class = '' + link_class = "" expected_attrs = ' class="new" rel="nofollow"' html = '
  • ' % (link_class, url) actual_raw = parse(html).annotateLinks(base_url=AL_BASE_URL).serialize() @@ -885,7 +896,7 @@ def test_dont_annotate_links_with_trailing_slashes(db, root_doc): should NOT become this: Root """ # Sanity check our fixture - assert Document.objects.filter(locale='en-US', slug='Root').exists() + assert Document.objects.filter(locale="en-US", slug="Root").exists() html = """
    • Root
    • @@ -905,8 +916,8 @@ def test_dont_annotate_links_with_trailing_slashes(db, root_doc): def test_annotate_links_uilocale_to_existing_doc(root_doc): """Links to existing docs with embeded locales are unmodified.""" - assert root_doc.get_absolute_url() == '/en-US/docs/Root' - url = '/en-US/docs/en-US/Root' # Notice the 'en-US' after '/docs/' + assert root_doc.get_absolute_url() == "/en-US/docs/Root" + url = "/en-US/docs/en-US/Root" # Notice the 'en-US' after '/docs/' html = normalize_html('
    • ' % url) actual_raw = parse(html).annotateLinks(base_url=AL_BASE_URL).serialize() assert normalize_html(actual_raw) == html @@ -914,23 +925,22 @@ def test_annotate_links_uilocale_to_existing_doc(root_doc): def test_annotate_links_uilocale_to_nonexisting_doc(db): """Links to new docs with embeded locales are modified.""" - url = '/en-US/docs/en-US/Root' # Notice the 'en-US' after '/docs/' + url = "/en-US/docs/en-US/Root" # Notice the 'en-US' after '/docs/' html = '
    • ' % url actual_raw = parse(html).annotateLinks(base_url=AL_BASE_URL).serialize() - expected = normalize_html( - '
    • ' % url) + expected = normalize_html('
    • ' % url) assert normalize_html(actual_raw) == expected -@pytest.mark.parametrize('attributes', ('', 'class="foobar" name="quux"')) +@pytest.mark.parametrize("attributes", ("", 'class="foobar" name="quux"')) def test_annotate_links_no_href(attributes): """Links without an href do not break the annotator.""" - html = normalize_html('
    • No href
    • ' % attributes) + html = normalize_html("
    • No href
    • " % attributes) actual_raw = parse(html).annotateLinks(base_url=AL_BASE_URL).serialize() assert normalize_html(actual_raw) == html -@pytest.mark.parametrize('slug', ('tag/foo', 'feeds/atom/all', 'templates')) +@pytest.mark.parametrize("slug", ("tag/foo", "feeds/atom/all", "templates")) def test_annotate_links_docs_but_not_wiki_urls(slug): """Links to /docs/ URLs that are not wiki docs are not annotated.""" html = normalize_html('
    • Other
    • ' % slug) @@ -938,7 +948,7 @@ def test_annotate_links_docs_but_not_wiki_urls(slug): assert normalize_html(actual_raw) == html -@pytest.mark.parametrize('slug', ('', '/dashboards/revisions')) +@pytest.mark.parametrize("slug", ("", "/dashboards/revisions")) def test_annotate_links_not_docs_urls(slug): """Links that are not /docs/ are not annotated.""" html = normalize_html('
    • Other
    • ' % slug) @@ -946,10 +956,10 @@ def test_annotate_links_not_docs_urls(slug): assert normalize_html(actual_raw) == html -@pytest.mark.parametrize('slug', ('root', 'ROOT', 'rOoT')) +@pytest.mark.parametrize("slug", ("root", "ROOT", "rOoT")) def test_annotate_links_case_insensitive(root_doc, slug): """Links to existing docs are case insensitive.""" - url = '/en-US/docs/' + slug + url = "/en-US/docs/" + slug assert url != root_doc.get_absolute_url() html = normalize_html('
    • ' % url) actual_raw = parse(html).annotateLinks(base_url=AL_BASE_URL).serialize() @@ -961,18 +971,19 @@ def test_annotate_links_collation_insensitive(db): Under MySQL's utf8_general_ci collation, é == e """ - accent = 'Récursion' - no_accent = 'Recursion' + accent = "Récursion" + no_accent = "Recursion" assert accent.lower() != no_accent.lower - Document.objects.create(locale='fr', slug='Glossaire/' + accent, - title=accent) + Document.objects.create(locale="fr", slug="Glossaire/" + accent, title=accent) html = normalize_html( - '
    • ' + - '
    • ' % no_accent) + '
    • ' + + '
    • ' % no_accent + ) actual_raw = parse(html).annotateLinks(base_url=AL_BASE_URL).serialize() expected = normalize_html( - '
    • ' + - '
    • ' % no_accent) + '
    • ' + + '
    • ' % no_accent + ) assert normalize_html(actual_raw) == expected @@ -982,7 +993,8 @@ def test_annotate_links_external_link(): actual_raw = parse(html).annotateLinks(base_url=AL_BASE_URL).serialize() expected = normalize_html( '
    • ' - 'External link.
    • ') + "External link." + ) assert normalize_html(actual_raw) == expected @@ -1011,72 +1023,83 @@ def test_editor_safety_filter(self):

      Header Three

      test

      """ - result_src = (kuma.wiki.content.parse(doc_src) - .filterEditorSafety() - .serialize()) + result_src = kuma.wiki.content.parse(doc_src).filterEditorSafety().serialize() assert normalize_html(expected_src) == normalize_html(result_src) @pytest.mark.parametrize( - 'tag', + "tag", # Sample of tags from ALLOWED_TAGS - ('address', - 'article', - 'code', - 'datagrid', - 'details', - 'dt', - 'figure', - 'h5', - 'mark', - 'output', - 'pre', - 'progress', - )) + ( + "address", + "article", + "code", + "datagrid", + "details", + "dt", + "figure", + "h5", + "mark", + "output", + "pre", + "progress", + ), +) def test_clean_content_allows_simple_tag(tag): """clean_content allows simple tags, id attribute.""" html = '<{tag} id="{id}">'.format( - tag=tag, id='sect1' if tag == 'h5' else 'foo') + tag=tag, id="sect1" if tag == "h5" else "foo" + ) assert clean_content(html) == html -@pytest.mark.parametrize( - 'tag', - ('br', - 'command', - 'img', - 'input', - )) +@pytest.mark.parametrize("tag", ("br", "command", "img", "input",)) def test_clean_content_allows_self_closed_tags(tag): """clean_content allows self-closed tags.""" - html = '<%s>' % tag + html = "<%s>" % tag assert clean_content(html) == html def test_clean_content_preserves_whitespace(): """clean_content allows an HTML table.""" - html = ('' - '
      foo
      foo
      ') + html = ( + "" + "
      foo
      foo
      " + ) assert clean_content(html) == html @pytest.mark.parametrize( - 'html,expected', - (('', - ''), - ((''), - ('picture of foo')), - ('foo', - 'foo'), - ('
      foo
      ', - '
      foo
      '), - ((''), - ('')), - )) + "html,expected", + ( + ('', ''), + ( + ( + '' + ), + ( + 'picture of foo' + ), + ), + ( + 'foo', + 'foo', + ), + ('
      foo
      ', '
      foo
      '), + ( + ( + '' + ), + ( + '' + ), + ), + ), +) def test_clean_content_allows_some_attributes(html, expected): """ clean_content allows attributes, orders them alphabetically, and @@ -1111,7 +1134,7 @@ def test_clean_content_iframe_in_script(): """script tags should not be allowed, and markup inside should be cleaned""" # Note that iframe tags _are_ allowed, but made safe; see ALLOWED_TAGS content = '' - expected = '<script></script>' + expected = "<script></script>" result = clean_content(content) assert normalize_html(expected) == normalize_html(result) @@ -1120,7 +1143,7 @@ def test_clean_content_iframe_in_style(): """style tags should not be allowed, and markup inside should be cleaned""" # Note that iframe tags _are_ allowed, but made safe; see ALLOWED_TAGS content = '' - expected = '<style></style>' + expected = "<style></style>" result = clean_content(content) assert normalize_html(expected) == normalize_html(result) @@ -1171,15 +1194,19 @@ def test_clean_removes_empty_paragraphs(): def test_extractor_css_classnames(root_doc, wiki_user): """The Extractor can return the CSS class names in use.""" - classes = ('foobar', 'barfoo', 'bazquux') - content = """ + classes = ("foobar", "barfoo", "bazquux") + content = ( + """

      Test

      Test

      Test
      No Class
      - """ % classes + """ + % classes + ) root_doc.current_revision = Revision.objects.create( - document=root_doc, content=content, creator=wiki_user) + document=root_doc, content=content, creator=wiki_user + ) root_doc.render() # Also saves result = root_doc.extract.css_classnames() assert sorted(result) == sorted(classes) @@ -1192,13 +1219,17 @@ def test_extractor_html_attributes(root_doc, wiki_user): 'id="frazzy"', 'lang="farb"', ) - content = """ + content = ( + """

      Test

      Test

      Test
      - """ % attributes + """ + % attributes + ) root_doc.current_revision = Revision.objects.create( - document=root_doc, content=content, creator=wiki_user) + document=root_doc, content=content, creator=wiki_user + ) root_doc.render() # Also saves result = root_doc.extract.html_attributes() assert sorted(result) == sorted(attributes) @@ -1206,26 +1237,30 @@ def test_extractor_html_attributes(root_doc, wiki_user): def test_extractor_macro_names(root_doc, wiki_user): """The Extractor can return the names of KumaScript macros.""" - macros = ('foobar', 'barfoo', 'bazquux', 'banana') - content = """ + macros = ("foobar", "barfoo", "bazquux", "banana") + content = ( + """

      {{ %s }}

      {{ %s("foo", "bar", "baz") }}

      {{ %s ("quux") }}

      {{%s}}

      - """ % macros + """ + % macros + ) root_doc.current_revision = Revision.objects.create( - document=root_doc, content=content, creator=wiki_user) + document=root_doc, content=content, creator=wiki_user + ) result = root_doc.extract.macro_names() assert sorted(result) == sorted(macros) -@pytest.mark.parametrize('is_rendered', (True, False)) -@pytest.mark.parametrize('method', ('macro_names', 'css_classnames', - 'html_attributes')) +@pytest.mark.parametrize("is_rendered", (True, False)) +@pytest.mark.parametrize("method", ("macro_names", "css_classnames", "html_attributes")) def test_extractor_no_content(method, is_rendered, root_doc, wiki_user): """The Extractor returns empty lists when the document has no content.""" root_doc.current_revision = Revision.objects.create( - document=root_doc, content='', creator=wiki_user) + document=root_doc, content="", creator=wiki_user + ) if is_rendered: root_doc.render() result = getattr(root_doc.extract, method)() @@ -1235,26 +1270,30 @@ def test_extractor_no_content(method, is_rendered, root_doc, wiki_user): def test_extractor_code_sample(root_doc, wiki_user): """The Extractor can return the sections of a code sample.""" code_sample = { - 'html': 'Some HTML', - 'css': '.some-css { color: red; }', - 'js': 'window.alert("HI THERE")', + "html": "Some HTML", + "css": ".some-css { color: red; }", + "js": 'window.alert("HI THERE")', } - content = """ + content = ( + """
      %(html)s
      %(css)s
      %(js)s
      {{ EmbedLiveSample('sample1') }} - """ % code_sample + """ + % code_sample + ) root_doc.current_revision = Revision.objects.create( - document=root_doc, content=content, creator=wiki_user) - result = root_doc.extract.code_sample('sample') + document=root_doc, content=content, creator=wiki_user + ) + result = root_doc.extract.code_sample("sample") assert result == code_sample def test_extractor_code_sample_unescape(root_doc, wiki_user): - '''The Extractor unescapes content in
       blocks.'''
      +    """The Extractor unescapes content in 
       blocks."""
           sample_html = """
               

      Hello world!

      @@ -1279,13 +1318,18 @@ def test_extractor_code_sample_unescape(root_doc, wiki_user):
      %s
    - """ % (escape(sample_html), escape(sample_css), escape(sample_js)) + """ % ( + escape(sample_html), + escape(sample_css), + escape(sample_js), + ) root_doc.current_revision = Revision.objects.create( - document=root_doc, content=content, creator=wiki_user) - result = root_doc.extract.code_sample('sample') - assert sample_html.strip() == result['html'].strip() - assert sample_css == result['css'] - assert sample_js == result['js'] + document=root_doc, content=content, creator=wiki_user + ) + result = root_doc.extract.code_sample("sample") + assert sample_html.strip() == result["html"].strip() + assert sample_css == result["css"] + assert sample_js == result["js"] def test_extractor_code_sample_nbsp_is_converted(root_doc, wiki_user): @@ -1308,41 +1352,50 @@ def test_extractor_code_sample_nbsp_is_converted(root_doc, wiki_user): """ root_doc.current_revision = Revision.objects.create( - document=root_doc, content=content, creator=wiki_user) - result = root_doc.extract.code_sample('With_nbsp') - assert '\xa0' not in result['css'] - assert ' ' not in result['css'] + document=root_doc, content=content, creator=wiki_user + ) + result = root_doc.extract.code_sample("With_nbsp") + assert "\xa0" not in result["css"] + assert " " not in result["css"] -@pytest.mark.parametrize('skip_part', ('html', 'css', 'js', 'all')) +@pytest.mark.parametrize("skip_part", ("html", "css", "js", "all")) def test_extractor_code_sample_missing_parts(root_doc, wiki_user, skip_part): """The Extractor returns None if a code sample section is missing.""" parts = {} expected = {} - for part in ('html', 'css', 'js'): - if skip_part in (part, 'all'): - parts[part] = '' + for part in ("html", "css", "js"): + if skip_part in (part, "all"): + parts[part] = "" expected[part] = None else: parts[part] = '
    included
    ' % part - expected[part] = 'included' - content = """ + expected[part] = "included" + content = ( + """

    Code Sample

    %(html)s %(css)s %(js)s - """ % parts + """ + % parts + ) root_doc.current_revision = Revision.objects.create( - document=root_doc, content=content, creator=wiki_user) - result = root_doc.extract.code_sample('Code_Sample') + document=root_doc, content=content, creator=wiki_user + ) + result = root_doc.extract.code_sample("Code_Sample") assert result == expected -@pytest.mark.parametrize('sample_id', ('Bug:1173170', # bug 1173170 - 'sam\x00ple', # bug 1269143 - """sam<'&">ple""", # bug 1269143 - )) +@pytest.mark.parametrize( + "sample_id", + ( + "Bug:1173170", # bug 1173170 + "sam\x00ple", # bug 1269143 + """sam<'&">ple""", # bug 1269143 + ), +) def test_extractor_code_sample_with_problem_id(root_doc, wiki_user, sample_id): """The Extractor does not error if the code sample ID is bad.""" content = """ @@ -1354,12 +1407,13 @@ def test_extractor_code_sample_with_problem_id(root_doc, wiki_user, sample_id): {{ EmbedLiveSample('sample1') }} """ root_doc.current_revision = Revision.objects.create( - document=root_doc, content=content, creator=wiki_user) + document=root_doc, content=content, creator=wiki_user + ) result = root_doc.extract.code_sample(sample_id) - assert result == {'html': None, 'css': None, 'js': None} + assert result == {"html": None, "css": None, "js": None} -@pytest.mark.parametrize('annotate_links', [True, False]) +@pytest.mark.parametrize("annotate_links", [True, False]) def test_extractor_section(root_doc, annotate_links): """The Extractor can extract a section, optionally annotating links.""" quick_links_template = """ @@ -1368,48 +1422,63 @@ def test_extractor_section(root_doc, annotate_links):
  • New
  • """ - quick_links = quick_links_template % '' + quick_links = quick_links_template % "" if annotate_links: expected = quick_links_template % 'rel="nofollow" class="new"' else: expected = quick_links - content = """ + content = ( + """
    - """ % quick_links - result = root_doc.extract.section(content, "Quick_Links", - annotate_links=annotate_links) + """ + % quick_links + ) + result = root_doc.extract.section( + content, "Quick_Links", annotate_links=annotate_links + ) assert normalize_html(result) == normalize_html(expected) -@pytest.mark.parametrize('wrapper', list(SUMMARY_PLUS_SEO_WRAPPERS.values()), - ids=list(SUMMARY_PLUS_SEO_WRAPPERS)) -@pytest.mark.parametrize('markup, text', list(SUMMARY_CONTENT.values()), - ids=list(SUMMARY_CONTENT)) +@pytest.mark.parametrize( + "wrapper", + list(SUMMARY_PLUS_SEO_WRAPPERS.values()), + ids=list(SUMMARY_PLUS_SEO_WRAPPERS), +) +@pytest.mark.parametrize( + "markup, text", list(SUMMARY_CONTENT.values()), ids=list(SUMMARY_CONTENT) +) def test_summary_section(markup, text, wrapper): content = wrapper.format(markup) - assert get_seo_description(content, 'en-US') == text - assert normalize_html(get_seo_description(content, 'en-US', False)) == normalize_html(markup) + assert get_seo_description(content, "en-US") == text + assert normalize_html( + get_seo_description(content, "en-US", False) + ) == normalize_html(markup) -@pytest.mark.parametrize('wrapper', list(SUMMARY_WRAPPERS.values()), - ids=list(SUMMARY_WRAPPERS)) -@pytest.mark.parametrize('markup, expected_markup, text', - list(SUMMARIES_SEO_CONTENT.values()), - ids=list(SUMMARIES_SEO_CONTENT)) +@pytest.mark.parametrize( + "wrapper", list(SUMMARY_WRAPPERS.values()), ids=list(SUMMARY_WRAPPERS) +) +@pytest.mark.parametrize( + "markup, expected_markup, text", + list(SUMMARIES_SEO_CONTENT.values()), + ids=list(SUMMARIES_SEO_CONTENT), +) def test_multiple_seo_summaries(markup, expected_markup, text, wrapper): content = wrapper.format(markup) - assert get_seo_description(content, 'en-US') == text - assert normalize_html(get_seo_description(content, 'en-US', False)) == normalize_html(expected_markup) + assert get_seo_description(content, "en-US") == text + assert normalize_html( + get_seo_description(content, "en-US", False) + ) == normalize_html(expected_markup) def test_empty_seo_summary(): content = '

    ' - assert get_seo_description(content, 'en-US', strip_markup=True) == '' - assert get_seo_description(content, 'en-US', strip_markup=False) == '' + assert get_seo_description(content, "en-US", strip_markup=True) == "" + assert get_seo_description(content, "en-US", strip_markup=False) == "" def test_empty_paragraph_content(): @@ -1421,7 +1490,7 @@ def test_empty_paragraph_content(): translate this page until it is done; it will be much easier at that point. The French translation is a test to be sure that it works well.

    """ - assert get_seo_description(content, 'en-US', False) == '' + assert get_seo_description(content, "en-US", False) == "" def test_content_is_a_url(mock_requests): @@ -1430,13 +1499,13 @@ def test_content_is_a_url(mock_requests): # My not setting up expectations, and if it got used, # these tests would raise a `NoMockAddress` exception. - url = 'https://developer.mozilla.org' - assert get_seo_description(url, 'en-US', False) == '' + url = "https://developer.mozilla.org" + assert get_seo_description(url, "en-US", False) == "" # Doesn't matter if it's http or https - assert get_seo_description(url.replace('s:/', ':/'), 'en-US', False) == '' + assert get_seo_description(url.replace("s:/", ":/"), "en-US", False) == "" # If the content, afterwards, has real paragraphs, then the first # line becomes the seo description - real_line = '\n

    This is the second line

    ' - assert get_seo_description(url + real_line, 'en-US', False) == url + real_line = "\n

    This is the second line

    " + assert get_seo_description(url + real_line, "en-US", False) == url diff --git a/kuma/wiki/tests/test_events.py b/kuma/wiki/tests/test_events.py index 87e62373293..923b75cbe81 100644 --- a/kuma/wiki/tests/test_events.py +++ b/kuma/wiki/tests/test_events.py @@ -8,31 +8,37 @@ from kuma.core.utils import order_params -from ..events import (EditDocumentEvent, first_edit_email, - notification_context, spam_attempt_email) +from ..events import ( + EditDocumentEvent, + first_edit_email, + notification_context, + spam_attempt_email, +) from ..models import DocumentSpamAttempt def test_notification_context_for_create(create_revision): """Test the notification context for a created English page.""" context = notification_context(create_revision) - utm_campaign = ('?utm_campaign=Wiki+Doc+Edits&utm_medium=email' - '&utm_source=developer.mozilla.org') - url = '/en-US/docs/Root' - user_url = reverse('users.user_detail', kwargs={'username': 'wiki_user'}) + utm_campaign = ( + "?utm_campaign=Wiki+Doc+Edits&utm_medium=email" + "&utm_source=developer.mozilla.org" + ) + url = "/en-US/docs/Root" + user_url = reverse("users.user_detail", kwargs={"username": "wiki_user"}) expected = { - 'compare_url': '', - 'creator': create_revision.creator, - 'diff': 'Diff is unavailable.', - 'document_title': 'Root Document', - 'locale': 'en-US' + "compare_url": "", + "creator": create_revision.creator, + "diff": "Diff is unavailable.", + "document_title": "Root Document", + "locale": "en-US", } expected_urls = { - 'edit_url': order_params(url + '$edit' + utm_campaign), - 'history_url': order_params(url + '$history' + utm_campaign), - 'user_url': order_params(user_url + utm_campaign), - 'view_url': order_params(url + utm_campaign) + "edit_url": order_params(url + "$edit" + utm_campaign), + "history_url": order_params(url + "$history" + utm_campaign), + "user_url": order_params(user_url + utm_campaign), + "view_url": order_params(url + utm_campaign), } assert_expectations_url(context, expected, expected_urls) @@ -41,14 +47,18 @@ def test_notification_context_for_create(create_revision): def test_notification_context_for_edit(create_revision, edit_revision): """Test the notification context for an edited English page.""" context = notification_context(edit_revision) - utm_campaign = ('?utm_campaign=Wiki+Doc+Edits&utm_medium=email' - '&utm_source=developer.mozilla.org') - url = '/en-US/docs/Root' - user_url = reverse('users.user_detail', kwargs={'username': 'wiki_user'}) - compare_url = (url + - "$compare?to=%d" % edit_revision.id + - "&from=%d" % create_revision.id + - utm_campaign.replace("?", "&")) + utm_campaign = ( + "?utm_campaign=Wiki+Doc+Edits&utm_medium=email" + "&utm_source=developer.mozilla.org" + ) + url = "/en-US/docs/Root" + user_url = reverse("users.user_detail", kwargs={"username": "wiki_user"}) + compare_url = ( + url + + "$compare?to=%d" % edit_revision.id + + "&from=%d" % create_revision.id + + utm_campaign.replace("?", "&") + ) diff = """\ --- [en-US] #%d @@ -63,21 +73,24 @@ def test_notification_context_for_edit(create_revision, edit_revision): + The root document.

    - """ % (create_revision.id, edit_revision.id) + """ % ( + create_revision.id, + edit_revision.id, + ) expected = { - 'creator': edit_revision.creator, - 'diff': diff, - 'document_title': 'Root Document', - 'locale': 'en-US' + "creator": edit_revision.creator, + "diff": diff, + "document_title": "Root Document", + "locale": "en-US", } expected_urls = { - 'compare_url': order_params(compare_url), - 'edit_url': order_params(url + '$edit' + utm_campaign), - 'history_url': order_params(url + '$history' + utm_campaign), - 'user_url': order_params(user_url + utm_campaign), - 'view_url': url + utm_campaign + "compare_url": order_params(compare_url), + "edit_url": order_params(url + "$edit" + utm_campaign), + "history_url": order_params(url + "$history" + utm_campaign), + "user_url": order_params(user_url + utm_campaign), + "view_url": url + utm_campaign, } assert_expectations_url(context, expected, expected_urls) @@ -86,14 +99,18 @@ def test_notification_context_for_edit(create_revision, edit_revision): def test_notification_context_for_translation(trans_revision, create_revision): """Test the notification context for a created English page.""" context = notification_context(trans_revision) - utm_campaign = ('?utm_campaign=Wiki+Doc+Edits&utm_medium=email' - '&utm_source=developer.mozilla.org') - url = '/fr/docs/Racine' - user_url = reverse('users.user_detail', kwargs={'username': 'wiki_user'}) - compare_url = (url + - "$compare?to=%d" % trans_revision.id + - "&from=%d" % create_revision.id + - utm_campaign.replace("?", "&")) + utm_campaign = ( + "?utm_campaign=Wiki+Doc+Edits&utm_medium=email" + "&utm_source=developer.mozilla.org" + ) + url = "/fr/docs/Racine" + user_url = reverse("users.user_detail", kwargs={"username": "wiki_user"}) + compare_url = ( + url + + "$compare?to=%d" % trans_revision.id + + "&from=%d" % create_revision.id + + utm_campaign.replace("?", "&") + ) diff = """\ --- [en-US] #%d @@ -108,72 +125,74 @@ def test_notification_context_for_translation(trans_revision, create_revision): + Mise en route...

    - """ % (create_revision.id, trans_revision.id) + """ % ( + create_revision.id, + trans_revision.id, + ) expected = { - 'creator': trans_revision.creator, - 'diff': diff, - 'document_title': 'Racine du Document', - 'locale': 'fr' + "creator": trans_revision.creator, + "diff": diff, + "document_title": "Racine du Document", + "locale": "fr", } expected_urls = { - 'compare_url': order_params(compare_url), - 'edit_url': order_params(url + '$edit' + utm_campaign), - 'history_url': order_params(url + '$history' + utm_campaign), - 'user_url': order_params(user_url + utm_campaign), - 'view_url': order_params(url + utm_campaign) + "compare_url": order_params(compare_url), + "edit_url": order_params(url + "$edit" + utm_campaign), + "history_url": order_params(url + "$history" + utm_campaign), + "user_url": order_params(user_url + utm_campaign), + "view_url": order_params(url + utm_campaign), } assert_expectations_url(context, expected, expected_urls) -@mock.patch('tidings.events.EventUnion.fire') -def test_edit_document_event_fires_union(mock_fire, create_revision, - wiki_user): +@mock.patch("tidings.events.EventUnion.fire") +def test_edit_document_event_fires_union(mock_fire, create_revision, wiki_user): """Test that EditDocumentEvent also notifies for the tree.""" EditDocumentEvent.notify(wiki_user, create_revision.document) EditDocumentEvent(create_revision).fire() mock_fire.assert_called_once_with() -@mock.patch('kuma.wiki.events.emails_with_users_and_watches') +@mock.patch("kuma.wiki.events.emails_with_users_and_watches") def test_edit_document_event_emails_on_create(mock_emails, create_revision): """Test event email parameters for creation of an English page.""" - users_and_watches = [('fake_user', [None])] + users_and_watches = [("fake_user", [None])] EditDocumentEvent(create_revision)._mails(users_and_watches) assert mock_emails.call_count == 1 args, kwargs = mock_emails.call_args assert not args assert kwargs == { - 'subject': mock.ANY, - 'text_template': 'wiki/email/edited.ltxt', - 'html_template': None, - 'context_vars': notification_context(create_revision), - 'users_and_watches': users_and_watches, - 'default_locale': 'en-US', - 'headers': { - 'X-Kuma-Editor-Username': 'wiki_user', - 'X-Kuma-Document-Url': create_revision.document.get_full_url(), - 'X-Kuma-Document-Title': 'Root Document', - 'X-Kuma-Document-Locale': 'en-US', - } + "subject": mock.ANY, + "text_template": "wiki/email/edited.ltxt", + "html_template": None, + "context_vars": notification_context(create_revision), + "users_and_watches": users_and_watches, + "default_locale": "en-US", + "headers": { + "X-Kuma-Editor-Username": "wiki_user", + "X-Kuma-Document-Url": create_revision.document.get_full_url(), + "X-Kuma-Document-Title": "Root Document", + "X-Kuma-Document-Locale": "en-US", + }, } - subject = kwargs['subject'] % kwargs['context_vars'] + subject = kwargs["subject"] % kwargs["context_vars"] expected = '[MDN][en-US][New] Page "Root Document" created by wiki_user' assert subject == expected -@mock.patch('kuma.wiki.events.emails_with_users_and_watches') +@mock.patch("kuma.wiki.events.emails_with_users_and_watches") def test_edit_document_event_emails_on_change(mock_emails, edit_revision): """Test event email parameters for changing an English page.""" - users_and_watches = [('fake_user', [None])] + users_and_watches = [("fake_user", [None])] EditDocumentEvent(edit_revision)._mails(users_and_watches) assert mock_emails.call_count == 1 args, kwargs = mock_emails.call_args assert not args context = notification_context(edit_revision) - assert kwargs['context_vars'] == context - subject = kwargs['subject'] % context + assert kwargs["context_vars"] == context + subject = kwargs["subject"] % context expected = '[MDN][en-US] Page "Root Document" changed by wiki_user' assert subject == expected @@ -181,33 +200,37 @@ def test_edit_document_event_emails_on_change(mock_emails, edit_revision): def test_first_edit_email_on_create(create_revision): """A first edit email is formatted for a new English page.""" mail = first_edit_email(create_revision) - assert mail.subject == ('[MDN][en-US][New] wiki_user made their first edit,' - ' creating: Root Document') + assert mail.subject == ( + "[MDN][en-US][New] wiki_user made their first edit," " creating: Root Document" + ) assert mail.extra_headers == { - 'X-Kuma-Editor-Username': 'wiki_user', - 'X-Kuma-Document-Url': create_revision.document.get_full_url(), - 'X-Kuma-Document-Title': 'Root Document', - 'X-Kuma-Document-Locale': 'en-US', + "X-Kuma-Editor-Username": "wiki_user", + "X-Kuma-Document-Url": create_revision.document.get_full_url(), + "X-Kuma-Document-Title": "Root Document", + "X-Kuma-Document-Locale": "en-US", } def test_first_edit_email_on_change(edit_revision): """A first edit email is formatted for an English change.""" mail = first_edit_email(edit_revision) - assert mail.subject == ('[MDN][en-US] wiki_user made their first edit,' - ' to: Root Document') + assert mail.subject == ( + "[MDN][en-US] wiki_user made their first edit," " to: Root Document" + ) def test_first_edit_email_on_translate(trans_revision): """A first edit email is formatted for a first translation.""" mail = first_edit_email(trans_revision) - assert mail.subject == ('[MDN][fr][New] wiki_user made their first edit,' - ' creating: Racine du Document') + assert mail.subject == ( + "[MDN][fr][New] wiki_user made their first edit," + " creating: Racine du Document" + ) assert mail.extra_headers == { - 'X-Kuma-Editor-Username': 'wiki_user', - 'X-Kuma-Document-Url': trans_revision.document.get_full_url(), - 'X-Kuma-Document-Title': 'Racine du Document', - 'X-Kuma-Document-Locale': 'fr', + "X-Kuma-Editor-Username": "wiki_user", + "X-Kuma-Document-Url": trans_revision.document.get_full_url(), + "X-Kuma-Document-Title": "Racine du Document", + "X-Kuma-Document-Locale": "fr", } @@ -215,35 +238,36 @@ def test_spam_attempt_email_on_create(wiki_user): """A spam attempt email is formatted for a new English page.""" spam_attempt = DocumentSpamAttempt( user=wiki_user, - title='My new spam page', - slug='my-new-spam-page', - created=datetime(2017, 4, 14, 15, 13) + title="My new spam page", + slug="my-new-spam-page", + created=datetime(2017, 4, 14, 15, 13), ) mail = spam_attempt_email(spam_attempt) - assert mail.subject == ('[MDN] Wiki spam attempt recorded with title' - ' My new spam page') - assert mail.extra_headers == { - 'X-Kuma-Editor-Username': 'wiki_user' - } + assert mail.subject == ( + "[MDN] Wiki spam attempt recorded with title" " My new spam page" + ) + assert mail.extra_headers == {"X-Kuma-Editor-Username": "wiki_user"} def test_spam_attempt_email_on_change(wiki_user, root_doc): """A spam attempt email is formatted for an English change.""" spam_attempt = DocumentSpamAttempt( user=wiki_user, - title='A spam revision', + title="A spam revision", slug=root_doc.slug, document=root_doc, - created=datetime(2017, 4, 14, 15, 14) + created=datetime(2017, 4, 14, 15, 14), ) mail = spam_attempt_email(spam_attempt) - assert mail.subject == ('[MDN] Wiki spam attempt recorded for document' - ' /en-US/docs/Root (Root Document)') + assert mail.subject == ( + "[MDN] Wiki spam attempt recorded for document" + " /en-US/docs/Root (Root Document)" + ) assert mail.extra_headers == { - 'X-Kuma-Editor-Username': 'wiki_user', - 'X-Kuma-Document-Url': root_doc.get_full_url(), - 'X-Kuma-Document-Title': 'Root Document', - 'X-Kuma-Document-Locale': 'en-US', + "X-Kuma-Editor-Username": "wiki_user", + "X-Kuma-Document-Url": root_doc.get_full_url(), + "X-Kuma-Document-Title": "Root Document", + "X-Kuma-Document-Locale": "en-US", } @@ -251,26 +275,25 @@ def test_spam_attempt_email_on_translate(wiki_user, trans_doc): """A spam attempt email is formatted for a new translation.""" spam_attempt = DocumentSpamAttempt( user=wiki_user, - title='Ma nouvelle page de spam', - slug='ma-nouvelle-page-de-spam', - created=datetime(2017, 4, 15, 10, 54) + title="Ma nouvelle page de spam", + slug="ma-nouvelle-page-de-spam", + created=datetime(2017, 4, 15, 10, 54), ) mail = spam_attempt_email(spam_attempt) - assert mail.subject == ('[MDN] Wiki spam attempt recorded with title' - ' Ma nouvelle page de spam') + assert mail.subject == ( + "[MDN] Wiki spam attempt recorded with title" " Ma nouvelle page de spam" + ) def test_spam_attempt_email_partial_model(wiki_user): """A spam attempt email is formatted with partial information.""" spam_attempt = DocumentSpamAttempt( - user=wiki_user, - slug='my-new-spam-page', - created=datetime(2017, 4, 14, 15, 13) + user=wiki_user, slug="my-new-spam-page", created=datetime(2017, 4, 14, 15, 13) ) mail = spam_attempt_email(spam_attempt) - assert mail.subject == ('[MDN] Wiki spam attempt recorded') + assert mail.subject == ("[MDN] Wiki spam attempt recorded") assert mail.extra_headers == { - 'X-Kuma-Editor-Username': 'wiki_user', + "X-Kuma-Editor-Username": "wiki_user", } diff --git a/kuma/wiki/tests/test_feeds.py b/kuma/wiki/tests/test_feeds.py index de9f94aefac..00cd47d5688 100644 --- a/kuma/wiki/tests/test_feeds.py +++ b/kuma/wiki/tests/test_feeds.py @@ -1,5 +1,3 @@ - - import json from datetime import datetime from urllib.parse import parse_qs, urlparse @@ -19,8 +17,9 @@ def test_l10n_updates_no_updates(trans_doc, client): """When translations are up-to-date, l10n-updates feed is empty.""" - feed_url = reverse('wiki.feeds.l10n_updates', locale=trans_doc.locale, - kwargs={'format': 'json'}) + feed_url = reverse( + "wiki.feeds.l10n_updates", locale=trans_doc.locale, kwargs={"format": "json"} + ) resp = client.get(feed_url) assert resp.status_code == 200 assert_shared_cache_header(resp) @@ -30,40 +29,44 @@ def test_l10n_updates_no_updates(trans_doc, client): def test_l10n_updates_parent_updated(trans_doc, edit_revision, client): """Out-of-date translations appear in the l10n-updates feed.""" - feed_url = reverse('wiki.feeds.l10n_updates', locale=trans_doc.locale, - kwargs={'format': 'json'}) + feed_url = reverse( + "wiki.feeds.l10n_updates", locale=trans_doc.locale, kwargs={"format": "json"} + ) resp = client.get(feed_url) assert resp.status_code == 200 assert_shared_cache_header(resp) data = json.loads(resp.content) assert len(data) == 1 - assert trans_doc.get_absolute_url() in data[0]['link'] + assert trans_doc.get_absolute_url() in data[0]["link"] -def test_l10n_updates_include_campaign(trans_doc, create_revision, - edit_revision, client): +def test_l10n_updates_include_campaign( + trans_doc, create_revision, edit_revision, client +): """Translation URLs include GA campaign data.""" - feed_url = reverse('wiki.feeds.l10n_updates', locale=trans_doc.locale, - kwargs={'format': 'rss'}) + feed_url = reverse( + "wiki.feeds.l10n_updates", locale=trans_doc.locale, kwargs={"format": "rss"} + ) resp = client.get(feed_url) assert resp.status_code == 200 assert_shared_cache_header(resp) feed = pq(resp.content) - items = feed.find('item') + items = feed.find("item") assert len(items) == 1 - desc_text = pq(items).find('description').text() + desc_text = pq(items).find("description").text() desc_html = pq(desc_text) # Description is encoded HTML - links = desc_html.find('a') + links = desc_html.find("a") assert len(links) == 3 for link in links: - href = link.attrib['href'] + href = link.attrib["href"] querystring = parse_qs(urlparse(href).query) - assert querystring['utm_campaign'] == ['feed'] - assert querystring['utm_medium'] == ['rss'] - assert querystring['utm_source'] == ['developer.mozilla.org'] + assert querystring["utm_campaign"] == ["feed"] + assert querystring["utm_medium"] == ["rss"] + assert querystring["utm_source"] == ["developer.mozilla.org"] -create_revision_rss = normalize_html(""" +create_revision_rss = normalize_html( + """

    Created by:

    wiki_user

    Content changes:

    @@ -77,10 +80,12 @@ def test_l10n_updates_include_campaign(trans_doc, create_revision, History -""") +""" +) # TODO: Investigate encoding issue w/

    Content changes -edit_revision_rss_template = normalize_html(""" +edit_revision_rss_template = normalize_html( + """

    Edited by:

    wiki_user

    Comment:

    @@ -144,23 +149,24 @@ def test_l10n_updates_include_campaign(trans_doc, create_revision, &utm_source=developer.mozilla.org">History -""") +""" +) def extract_description(feed_item): """Extract the description and diff ID (if set) from a feed item.""" - desc_text = pq(feed_item).find('description').text() + desc_text = pq(feed_item).find("description").text() desc = pq(desc_text) - table = desc.find('table.diff') + table = desc.find("table.diff") if table: # Format is difflib_chg_to[DIFF ID]__top assert len(table) == 1 - table_id = table[0].attrib['id'] - prefix = 'difflib_chg_to' - suffix = '__top' + table_id = table[0].attrib["id"] + prefix = "difflib_chg_to" + suffix = "__top" assert table_id.startswith(prefix) assert table_id.endswith(suffix) - diff_id = int(table_id[len(prefix):-len(suffix)]) + diff_id = int(table_id[len(prefix) : -len(suffix)]) else: diff_id = None return normalize_html(desc_text), diff_id @@ -168,46 +174,48 @@ def extract_description(feed_item): def test_recent_revisions(create_revision, edit_revision, client): """The revisions feed includes recent revisions.""" - feed_url = reverse('wiki.feeds.recent_revisions', - kwargs={'format': 'rss'}) + feed_url = reverse("wiki.feeds.recent_revisions", kwargs={"format": "rss"}) resp = client.get(feed_url) assert resp.status_code == 200 assert_shared_cache_header(resp) feed = pq(resp.content) - items = feed.find('item') + items = feed.find("item") assert len(items) == 2 desc1, diff_id1 = extract_description(items[0]) assert desc1 == create_revision_rss assert diff_id1 is None desc2, diff_id2 = extract_description(items[1]) - expected = edit_revision_rss_template % {'from_id': create_revision.id, - 'to_id': edit_revision.id, - 'diff_id': diff_id2} + expected = edit_revision_rss_template % { + "from_id": create_revision.id, + "to_id": edit_revision.id, + "diff_id": diff_id2, + } assert desc2 == expected def test_recent_revisions_pages(create_revision, edit_revision, client): """The revisions feed can be paginated.""" - feed_url = reverse('wiki.feeds.recent_revisions', - kwargs={'format': 'rss'}) - resp = client.get(feed_url, {'limit': 1}) + feed_url = reverse("wiki.feeds.recent_revisions", kwargs={"format": "rss"}) + resp = client.get(feed_url, {"limit": 1}) assert resp.status_code == 200 assert_shared_cache_header(resp) feed = pq(resp.content) - items = feed.find('item') + items = feed.find("item") assert len(items) == 1 desc_text, diff_id = extract_description(items[0]) - expected = edit_revision_rss_template % {'from_id': create_revision.id, - 'to_id': edit_revision.id, - 'diff_id': diff_id} + expected = edit_revision_rss_template % { + "from_id": create_revision.id, + "to_id": edit_revision.id, + "diff_id": diff_id, + } assert desc_text == expected - resp = client.get(feed_url, {'limit': 1, 'page': 2}) + resp = client.get(feed_url, {"limit": 1, "page": 2}) assert resp.status_code == 200 assert_shared_cache_header(resp) feed = pq(resp.content) - items = feed.find('item') + items = feed.find("item") assert len(items) == 1 desc_text2, diff_id2 = extract_description(items[0]) assert desc_text2 == create_revision_rss @@ -220,32 +228,31 @@ def test_recent_revisions_limit_0(edit_revision, client): TODO: the limit should probably be MAX_FEED_ITEMS instead, and applied before the start and finish positions are picked. """ - feed_url = reverse('wiki.feeds.recent_revisions', - kwargs={'format': 'rss'}) - resp = client.get(feed_url, {'limit': 0}) + feed_url = reverse("wiki.feeds.recent_revisions", kwargs={"format": "rss"}) + resp = client.get(feed_url, {"limit": 0}) assert resp.status_code == 200 assert_shared_cache_header(resp) feed = pq(resp.content) - items = feed.find('item') + items = feed.find("item") assert len(items) == 0 def test_recent_revisions_all_locales(trans_edit_revision, client, settings): """The ?all_locales parameter returns mixed locales (bug 869301).""" - host = 'example.com' + host = "example.com" settings.ALLOWED_HOSTS.append(host) - feed_url = reverse('wiki.feeds.recent_revisions', kwargs={'format': 'rss'}) - resp = client.get(feed_url, {'all_locales': ''}, - HTTP_HOST=host, - HTTP_X_FORWARDED_PROTO='https') + feed_url = reverse("wiki.feeds.recent_revisions", kwargs={"format": "rss"}) + resp = client.get( + feed_url, {"all_locales": ""}, HTTP_HOST=host, HTTP_X_FORWARDED_PROTO="https" + ) assert resp.status_code == 200 assert_shared_cache_header(resp) feed = pq(resp.content) - items = feed.find('item') + items = feed.find("item") assert len(items) == 4 # Test that links use host domain - actual_links = [pq(item).find('link').text() for item in items] + actual_links = [pq(item).find("link").text() for item in items] actual_domains = [urlparse(link).netloc for link in actual_links] assert actual_domains == [host] * 4 @@ -268,31 +275,34 @@ def test_recent_revisions_diff_includes_tags(create_revision, client): title=create_revision.title, content=create_revision.content, creator=create_revision.creator, - tags='"NewTag"' + tags='"NewTag"', ) - new_revision.review_tags.add('editorial') - feed_url = reverse('wiki.feeds.recent_revisions', kwargs={'format': 'rss'}) + new_revision.review_tags.add("editorial") + feed_url = reverse("wiki.feeds.recent_revisions", kwargs={"format": "rss"}) resp = client.get(feed_url) assert resp.status_code == 200 assert_shared_cache_header(resp) feed = pq(resp.content) - items = feed.find('item') + items = feed.find("item") assert len(items) == 2 - desc1, desc2 = [pq(item).find('description').text() for item in items] - assert 'Edited' not in desc1 # Created revision - assert 'Edited' in desc2 # New revision - assert '

    Tag changes:

    ' in desc2 - assert ('"NewTag"') in desc2 - assert '

    Review changes:

    ' in desc2 - assert ('editorial') in desc2 + desc1, desc2 = [pq(item).find("description").text() for item in items] + assert "Edited" not in desc1 # Created revision + assert "Edited" in desc2 # New revision + assert "

    Tag changes:

    " in desc2 + assert ( + '"NewTag"' + ) in desc2 + assert "

    Review changes:

    " in desc2 + assert ( + 'editorial' + ) in desc2 def test_recent_revisions_feed_ignores_render(edit_revision, client): """Re-rendering a document does not update the feed.""" - feed_url = reverse('wiki.feeds.recent_documents', - args=(), kwargs={'format': 'rss'}) + feed_url = reverse("wiki.feeds.recent_documents", args=(), kwargs={"format": "rss"}) resp = client.get(feed_url) assert resp.status_code == 200 assert_shared_cache_header(resp) @@ -307,90 +317,87 @@ def test_recent_revisions_feed_ignores_render(edit_revision, client): # Create a new edit, RSS feed changes edit_revision.document.revisions.create( title=edit_revision.title, - content=edit_revision.content + '\n

    New Line

    ', - creator=edit_revision.creator) + content=edit_revision.content + "\n

    New Line

    ", + creator=edit_revision.creator, + ) resp = client.get(feed_url) assert resp.content != start_content def test_recent_revisions_feed_omits_docs_without_rev(edit_revision, client): """Documents without a current revision are omitted from the feed.""" - feed_url = reverse('wiki.feeds.recent_documents', - args=(), kwargs={'format': 'rss'}) + feed_url = reverse("wiki.feeds.recent_documents", args=(), kwargs={"format": "rss"}) resp = client.get(feed_url) assert resp.status_code == 200 assert_shared_cache_header(resp) feed = pq(resp.content) - items = feed.find('item') + items = feed.find("item") assert len(items) == 1 - Document.objects.create(locale='en-US', slug='NoCurrentRev', - title='No Current Rev') + Document.objects.create(locale="en-US", slug="NoCurrentRev", title="No Current Rev") resp = client.get(feed_url) assert resp.status_code == 200 feed = pq(resp.content) - items = feed.find('item') + items = feed.find("item") assert len(items) == 1 -@pytest.mark.parametrize("locale", ('en-US', 'fr')) -def test_recent_revisions_feed_filter_by_locale(locale, trans_edit_revision, - client): +@pytest.mark.parametrize("locale", ("en-US", "fr")) +def test_recent_revisions_feed_filter_by_locale(locale, trans_edit_revision, client): """The recent revisions feed can be filtered by locale.""" - feed_url = reverse('wiki.feeds.recent_revisions', locale=locale, - kwargs={'format': 'json'}) + feed_url = reverse( + "wiki.feeds.recent_revisions", locale=locale, kwargs={"format": "json"} + ) resp = client.get(feed_url) assert resp.status_code == 200 assert_shared_cache_header(resp) data = json.loads(resp.content) assert len(data) == 2 for item in data: - path = urlparse(item['link']).path - assert path.startswith('/' + locale + '/') + path = urlparse(item["link"]).path + assert path.startswith("/" + locale + "/") -@pytest.mark.parametrize("locale", ('en-US', 'fr')) -def test_recent_documents_feed_filter_by_locale(locale, trans_edit_revision, - client): +@pytest.mark.parametrize("locale", ("en-US", "fr")) +def test_recent_documents_feed_filter_by_locale(locale, trans_edit_revision, client): """The recent documents feed can be filtered by locale.""" - feed_url = reverse('wiki.feeds.recent_documents', locale=locale, - kwargs={'format': 'json'}) + feed_url = reverse( + "wiki.feeds.recent_documents", locale=locale, kwargs={"format": "json"} + ) resp = client.get(feed_url) assert resp.status_code == 200 assert_shared_cache_header(resp) data = json.loads(resp.content) assert len(data) == 1 - path = urlparse(data[0]['link']).path - assert path.startswith('/' + locale + '/') + path = urlparse(data[0]["link"]).path + assert path.startswith("/" + locale + "/") def test_recent_documents_atom_feed(root_doc, client): """The recent documents feed can be formatted as an Atom feed.""" - feed_url = reverse('wiki.feeds.recent_documents', - kwargs={'format': 'atom'}) + feed_url = reverse("wiki.feeds.recent_documents", kwargs={"format": "atom"}) resp = client.get(feed_url) assert resp.status_code == 200 assert_shared_cache_header(resp) - assert resp['Content-Type'] == 'application/atom+xml; charset=utf-8' + assert resp["Content-Type"] == "application/atom+xml; charset=utf-8" def test_recent_documents_as_jsonp(root_doc, client): """The recent documents feed can be called with a JSONP wrapper.""" - feed_url = reverse('wiki.feeds.recent_documents', - kwargs={'format': 'json'}) + feed_url = reverse("wiki.feeds.recent_documents", kwargs={"format": "json"}) resp = client.get(feed_url) assert resp.status_code == 200 assert_shared_cache_header(resp) raw_json = resp.content - resp = client.get(feed_url, {'callback': 'jsonp_callback'}) + resp = client.get(feed_url, {"callback": "jsonp_callback"}) assert resp.status_code == 200 assert_shared_cache_header(resp) wrapped = resp.content - assert wrapped == b'jsonp_callback(%s)' % raw_json + assert wrapped == b"jsonp_callback(%s)" % raw_json # Invalid callback names are rejected - resp = client.get(feed_url, {'callback': 'try'}) + resp = client.get(feed_url, {"callback": "try"}) assert resp.status_code == 200 assert_shared_cache_header(resp) assert resp.content == raw_json @@ -398,28 +405,28 @@ def test_recent_documents_as_jsonp(root_doc, client): def test_recent_documents_optional_items(create_revision, client, settings): """The recent documents JSON feed includes some items if set.""" - feed_url = reverse('wiki.feeds.recent_documents', - kwargs={'format': 'json'}) + feed_url = reverse("wiki.feeds.recent_documents", kwargs={"format": "json"}) resp = client.get(feed_url) assert resp.status_code == 200 assert_shared_cache_header(resp) data = json.loads(resp.content) assert len(data) == 1 - assert data[0]['author_avatar'] == settings.DEFAULT_AVATAR - assert 'summary' not in data[0] + assert data[0]["author_avatar"] == settings.DEFAULT_AVATAR + assert "summary" not in data[0] - create_revision.summary = 'The summary' + create_revision.summary = "The summary" create_revision.save() resp = client.get(feed_url) assert resp.status_code == 200 data = json.loads(resp.content) - assert data[0]['summary'] == 'The summary' + assert data[0]["summary"] == "The summary" def test_recent_documents_feed_filter_by_tag(edit_revision, client): """The recent documents feed can be filtered by tag.""" - feed_url = reverse('wiki.feeds.recent_documents', - kwargs={'format': 'json', 'tag': 'TheTag'}) + feed_url = reverse( + "wiki.feeds.recent_documents", kwargs={"format": "json", "tag": "TheTag"} + ) resp = client.get(feed_url) assert resp.status_code == 200 assert_shared_cache_header(resp) @@ -430,7 +437,8 @@ def test_recent_documents_feed_filter_by_tag(edit_revision, client): title=edit_revision.title, content=edit_revision.content, creator=edit_revision.creator, - tags='"TheTag"') + tags='"TheTag"', + ) resp = client.get(feed_url) assert resp.status_code == 200 data = json.loads(resp.content) @@ -440,25 +448,27 @@ def test_recent_documents_feed_filter_by_tag(edit_revision, client): @pytest.mark.tags def test_feeds_update_after_doc_tag_change(client, wiki_user, root_doc): """Tag feeds should be updated after document tags change""" - tags1 = ['foo', 'bar', 'js'] - tags2 = ['lorem', 'ipsum'] + tags1 = ["foo", "bar", "js"] + tags2 = ["lorem", "ipsum"] # Create a revision with some tags - Revision.objects.create(document=root_doc, tags=','.join(tags1), creator=wiki_user) + Revision.objects.create(document=root_doc, tags=",".join(tags1), creator=wiki_user) # Create another revision with some other tags - Revision.objects.create(document=root_doc, tags=','.join(tags2), creator=wiki_user) + Revision.objects.create(document=root_doc, tags=",".join(tags2), creator=wiki_user) # Check document is latest tags feed for tag in tags2: - response = client.get(reverse('wiki.feeds.recent_documents', - args=['atom', tag]), follow=True) + response = client.get( + reverse("wiki.feeds.recent_documents", args=["atom", tag]), follow=True + ) assert response.status_code == 200 assert root_doc.title in response.content.decode(response.charset) # Check document is not in the previous tags feed for tag in tags1: - response = client.get(reverse('wiki.feeds.recent_documents', - args=['atom', tag]), follow=True) + response = client.get( + reverse("wiki.feeds.recent_documents", args=["atom", tag]), follow=True + ) assert response.status_code == 200 assert root_doc.title not in response.content.decode(response.charset) @@ -471,11 +481,11 @@ def test_recent_documents_handles_ambiguous_time(root_doc, client): root_doc.current_revision = Revision.objects.create( document=root_doc, creator=root_doc.current_revision.creator, - content='

    Happy Daylight Savings Time!

    ', + content="

    Happy Daylight Savings Time!

    ", title=root_doc.title, - created=ambiguous) - feed_url = reverse('wiki.feeds.recent_documents', - kwargs={'format': 'json'}) + created=ambiguous, + ) + feed_url = reverse("wiki.feeds.recent_documents", kwargs={"format": "json"}) resp = client.get(feed_url) assert resp.status_code == 200 assert_shared_cache_header(resp) @@ -485,14 +495,14 @@ def test_recent_documents_handles_ambiguous_time(root_doc, client): def test_list_review(edit_revision, client): """The documents needing review feed shows documents needing any review.""" - feed_url = reverse('wiki.feeds.list_review', kwargs={'format': 'json'}) + feed_url = reverse("wiki.feeds.list_review", kwargs={"format": "json"}) resp = client.get(feed_url) assert resp.status_code == 200 assert_shared_cache_header(resp) data = json.loads(resp.content) assert len(data) == 0 - edit_revision.review_tags.add('editorial') + edit_revision.review_tags.add("editorial") resp = client.get(feed_url) assert resp.status_code == 200 data = json.loads(resp.content) @@ -501,15 +511,16 @@ def test_list_review(edit_revision, client): def test_list_review_tag(edit_revision, client): """The documents needing editorial review feed works.""" - feed_url = reverse('wiki.feeds.list_review_tag', - kwargs={'format': 'json', 'tag': 'editorial'}) + feed_url = reverse( + "wiki.feeds.list_review_tag", kwargs={"format": "json", "tag": "editorial"} + ) resp = client.get(feed_url) assert resp.status_code == 200 assert_shared_cache_header(resp) data = json.loads(resp.content) assert len(data) == 0 - edit_revision.review_tags.add('editorial') + edit_revision.review_tags.add("editorial") resp = client.get(feed_url) assert resp.status_code == 200 data = json.loads(resp.content) @@ -522,7 +533,7 @@ def test_documentjsonfeedgenerator_encode(): TODO: The function should raise TypeError instead of returning None. """ - generator = DocumentJSONFeedGenerator('Title', '/feed', 'Description') + generator = DocumentJSONFeedGenerator("Title", "/feed", "Description") dt = datetime(2017, 12, 21, 22, 25) - assert generator._encode_complex(dt) == '2017-12-21T22:25:00' + assert generator._encode_complex(dt) == "2017-12-21T22:25:00" assert generator._encode_complex(dt.date()) is None diff --git a/kuma/wiki/tests/test_forms.py b/kuma/wiki/tests/test_forms.py index 824580149f1..9bdbc3eee81 100644 --- a/kuma/wiki/tests/test_forms.py +++ b/kuma/wiki/tests/test_forms.py @@ -11,9 +11,14 @@ from waffle.testutils import override_flag, override_switch from kuma.core.urlresolvers import reverse -from kuma.spam.constants import (CHECK_URL, SPAM_ADMIN_FLAG, SPAM_CHECKS_FLAG, - SPAM_SPAMMER_FLAG, SPAM_TESTING_FLAG, - VERIFY_URL) +from kuma.spam.constants import ( + CHECK_URL, + SPAM_ADMIN_FLAG, + SPAM_CHECKS_FLAG, + SPAM_SPAMMER_FLAG, + SPAM_TESTING_FLAG, + VERIFY_URL, +) from kuma.users.tests import UserTestCase from ..constants import SPAM_TRAINING_SWITCH @@ -24,30 +29,32 @@ class AkismetHistoricalDataTests(UserTestCase): """Tests for AkismetHistoricalData.""" + rf = RequestFactory() base_akismet_payload = { - 'blog_charset': 'UTF-8', - 'blog_lang': 'en_us', - 'comment_author': 'Test User', - 'comment_author_email': 'testuser@test.com', - 'comment_content': ( - 'Sample\n' - 'SampleSlug\n' - 'content\n' - 'Comment' - ), - 'comment_type': 'wiki-revision', - 'referrer': '', - 'user_agent': '', - 'user_ip': '0.0.0.0' + "blog_charset": "UTF-8", + "blog_lang": "en_us", + "comment_author": "Test User", + "comment_author_email": "testuser@test.com", + "comment_content": ("Sample\n" "SampleSlug\n" "content\n" "Comment"), + "comment_type": "wiki-revision", + "referrer": "", + "user_agent": "", + "user_ip": "0.0.0.0", } def setUp(self): super(AkismetHistoricalDataTests, self).setUp() - self.user = self.user_model.objects.get(username='testuser') - self.revision = revision(save=True, content='content', title='Sample', - slug='SampleSlug', comment='Comment', - summary='', tags='') + self.user = self.user_model.objects.get(username="testuser") + self.revision = revision( + save=True, + content="content", + title="Sample", + slug="SampleSlug", + comment="Comment", + summary="", + tags="", + ) def test_no_revision_ip_no_request(self): """ @@ -64,18 +71,24 @@ def test_revision_ip_no_data(self): This is a possible payload from an April 2016 revision. """ - RevisionIP.objects.create(revision=self.revision, ip='127.0.0.1', - user_agent='Agent', referrer='Referrer') - request = self.rf.get('/en-US/dashboard/revisions') + RevisionIP.objects.create( + revision=self.revision, + ip="127.0.0.1", + user_agent="Agent", + referrer="Referrer", + ) + request = self.rf.get("/en-US/dashboard/revisions") params = AkismetHistoricalData(self.revision, request).parameters expected = self.base_akismet_payload.copy() - expected.update({ - 'blog': 'http://testserver/', - 'permalink': 'http://testserver/en-US/docs/SampleSlug', - 'referrer': 'Referrer', - 'user_agent': 'Agent', - 'user_ip': '127.0.0.1', - }) + expected.update( + { + "blog": "http://testserver/", + "permalink": "http://testserver/en-US/docs/SampleSlug", + "referrer": "Referrer", + "user_agent": "Agent", + "user_ip": "127.0.0.1", + } + ) assert params == expected def test_revision_ip_with_data(self): @@ -84,20 +97,29 @@ def test_revision_ip_with_data(self): This payload is from a revision after April 2016. """ - RevisionIP.objects.create(revision=self.revision, ip='127.0.0.1', - user_agent='Agent', referrer='Referrer', - data='{"content": "spammy"}') - request = self.rf.get('/en-US/dashboard/revisions') + RevisionIP.objects.create( + revision=self.revision, + ip="127.0.0.1", + user_agent="Agent", + referrer="Referrer", + data='{"content": "spammy"}', + ) + request = self.rf.get("/en-US/dashboard/revisions") params = AkismetHistoricalData(self.revision, request).parameters - assert params == {'content': 'spammy'} + assert params == {"content": "spammy"} -@pytest.mark.parametrize('content,expected', [ - ('
    click me
    ', - '
    click me
    '), - ('', - '<svg><circle onload=confirm(3)>') -], ids=('strip', 'escape')) +@pytest.mark.parametrize( + "content,expected", + [ + ("
    click me
    ", "
    click me
    "), + ( + "", + "<svg><circle onload=confirm(3)>", + ), + ], + ids=("strip", "escape"), +) def test_form_onload_attr_filter(root_doc, rf, content, expected): """ For a RevisionForm created from an existing instance, the content should @@ -107,9 +129,9 @@ def test_form_onload_attr_filter(root_doc, rf, content, expected): rev = root_doc.current_revision rev.content = content rev.save() - request = rf.get('/') + request = rf.get("/") rev_form = RevisionForm(instance=rev, request=request) - assert rev_form.initial['content'] == expected + assert rev_form.initial["content"] == expected def test_form_loaded_with_section(root_doc, rf): @@ -137,10 +159,9 @@ def test_form_loaded_with_section(root_doc, rf):

    test

    test

    """ - request = rf.get('/') - rev_form = RevisionForm(instance=rev, section_id='s2', request=request) - assert (normalize_html(expected) == - normalize_html(rev_form.initial['content'])) + request = rf.get("/") + rev_form = RevisionForm(instance=rev, section_id="s2", request=request) + assert normalize_html(expected) == normalize_html(rev_form.initial["content"]) def test_form_save_section(root_doc, rf): @@ -175,10 +196,14 @@ def test_form_save_section(root_doc, rf):

    test

    test

    """ - request = rf.get('/') + request = rf.get("/") request.user = rev.creator - rev_form = RevisionForm(data={'content': replace_content}, instance=rev, - section_id='s2', request=request) + rev_form = RevisionForm( + data={"content": replace_content}, + instance=rev, + section_id="s2", + request=request, + ) new_rev = rev_form.save(rev.document) assert normalize_html(expected) == normalize_html(new_rev.content) @@ -189,92 +214,82 @@ def test_form_rejects_empty_slugs_with_parent(wiki_user, rf): portion. """ data = { - 'slug': '', - 'title': 'Title', - 'content': 'Content', + "slug": "", + "title": "Title", + "content": "Content", } - request = rf.get('/') + request = rf.get("/") request.user = wiki_user - rev_form = RevisionForm(data=data, - request=request, - parent_slug='User:groovecoder') + rev_form = RevisionForm(data=data, request=request, parent_slug="User:groovecoder") assert not rev_form.is_valid() def test_multiword_tags(root_doc, rf): """ Multi-word tags should be handled. """ rev = root_doc.current_revision - request = rf.get('/') + request = rf.get("/") request.user = rev.creator data = { - 'content': 'Content', - 'toc_depth': 1, - 'tags': '"MDN Meta"', + "content": "Content", + "toc_depth": 1, + "tags": '"MDN Meta"', } rev_form = RevisionForm(data=data, instance=rev, request=request) assert rev_form.is_valid() - assert rev_form.cleaned_data['tags'] == '"MDN Meta"' + assert rev_form.cleaned_data["tags"] == '"MDN Meta"' def test_revision_form_normalize_unicode(root_doc, rf): """Revision slugs are normalized to NFKC, required for URLs.""" - raw_slug = 'Εφαρμογές' # "Applications" in Greek (el) + raw_slug = "Εφαρμογές" # "Applications" in Greek (el) # In NFC / NFKD, 'έ' is represented by two "decomposed" codepoints # 03B5 (GREEK SMALL LETTER EPSILON) # 0301 (COMBINING ACUTE ACCENT) - nfkd_slug = unicodedata.normalize('NFKD', raw_slug) + nfkd_slug = unicodedata.normalize("NFKD", raw_slug) # In NFC / NFKC, 'έ' is represented by a "composed" codepoint # 03AD (GREEK SMALL LETTER EPSILON WITH TONOS) - nfkc_slug = unicodedata.normalize('NFKC', raw_slug) + nfkc_slug = unicodedata.normalize("NFKC", raw_slug) assert nfkd_slug != nfkc_slug rev = root_doc.current_revision - request = rf.get('/') + request = rf.get("/") request.user = rev.creator - data = { - 'content': 'Content', - 'toc_depth': 1, - 'slug': nfkd_slug - } + data = {"content": "Content", "toc_depth": 1, "slug": nfkd_slug} rev_form = RevisionForm(data=data, instance=rev, request=request) assert rev_form.is_valid() - assert rev_form.cleaned_data['slug'] == nfkc_slug + assert rev_form.cleaned_data["slug"] == nfkc_slug def test_document_form_normalize_unicode(root_doc, rf): """Document slugs are normalized to NFC, required for URLs.""" - raw_slug = 'ফায়ারফক্স' # "Firefox" in Bengali (bn) + raw_slug = "ফায়ারফক্স" # "Firefox" in Bengali (bn) # This slug is the same in NFC, NFD, NFKD, and NFKD. The second character # has these codepoints: # 09af BENGALI LETTER YA (য) # 09bc BENGALI SIGN NUKTA # 09be BENGALI VOWEL SIGN AA (non-breaking spacing mark) - nfkc_slug = '\u09ab\u09be\u09af\u09bc\u09be\u09b0\u09ab\u0995\u09cd\u09b8' - assert nfkc_slug == unicodedata.normalize('NFKC', raw_slug) + nfkc_slug = "\u09ab\u09be\u09af\u09bc\u09be\u09b0\u09ab\u0995\u09cd\u09b8" + assert nfkc_slug == unicodedata.normalize("NFKC", raw_slug) # An alternate representation of the second character is: # 09df BENGALI LETTER YYA (য়) # 09be BENGALI VOWEL SIGN AA (non-breaking spacing mark) - alt_slug = '\u09ab\u09be\u09df\u09be\u09b0\u09ab\u0995\u09cd\u09b8' + alt_slug = "\u09ab\u09be\u09df\u09be\u09b0\u09ab\u0995\u09cd\u09b8" assert alt_slug != nfkc_slug rev = root_doc.current_revision - request = rf.get('/') + request = rf.get("/") request.user = rev.creator - data = { - 'slug': alt_slug, - 'title': root_doc.title, - 'locale': root_doc.locale - } + data = {"slug": alt_slug, "title": root_doc.title, "locale": root_doc.locale} doc_form = DocumentForm(data=data, instance=root_doc) assert doc_form.is_valid() - assert doc_form.cleaned_data['slug'] == nfkc_slug + assert doc_form.cleaned_data["slug"] == nfkc_slug def test_case_sensitive_tags(root_doc, rf): @@ -285,44 +300,44 @@ def test_case_sensitive_tags(root_doc, rf): rev = root_doc.current_revision rev.tags = '"JavaScript"' rev.save() - request = rf.get('/') + request = rf.get("/") request.user = rev.creator data = { - 'content': 'Content', - 'toc_depth': 1, - 'tags': 'Javascript', # Note the lower-case "S". + "content": "Content", + "toc_depth": 1, + "tags": "Javascript", # Note the lower-case "S". } rev_form = RevisionForm(data=data, instance=rev, request=request) assert rev_form.is_valid() - assert rev_form.cleaned_data['tags'] == '"JavaScript"' + assert rev_form.cleaned_data["tags"] == '"JavaScript"' -@override_config(AKISMET_KEY='forms') +@override_config(AKISMET_KEY="forms") class RevisionFormViewTests(UserTestCase): """Setup tests for RevisionForm as used in views.""" + rf = RequestFactory() akismet_keys = [ # Keys for a new English page or new translation - 'REMOTE_ADDR', - 'blog', - 'blog_charset', - 'blog_lang', - 'comment_author', - 'comment_author_email', - 'comment_content', - 'comment_type', - 'referrer', - 'user_agent', - 'user_ip', + "REMOTE_ADDR", + "blog", + "blog_charset", + "blog_lang", + "comment_author", + "comment_author_email", + "comment_content", + "comment_type", + "referrer", + "user_agent", + "user_ip", ] # Keys for a page edit (English or translation) - akismet_keys_edit = sorted(akismet_keys + ['permalink']) + akismet_keys_edit = sorted(akismet_keys + ["permalink"]) def setUp(self): super(RevisionFormViewTests, self).setUp() - self.testuser = self.user_model.objects.get(username='testuser') + self.testuser = self.user_model.objects.get(username="testuser") self.spam_checks_flag, created = Flag.objects.update_or_create( - name=SPAM_CHECKS_FLAG, - defaults={'everyone': True}, + name=SPAM_CHECKS_FLAG, defaults={"everyone": True}, ) def tearDown(self): @@ -338,44 +353,48 @@ class RevisionFormEditTests(RevisionFormViewTests): """ original = { # Default attributes of original revision - 'content': ( + "content": ( '

    Summary

    \n' - '

    The display CSS property' - ' specifies the type of rendering box used for an element.

    \n' - '

    {{cssinfo}}

    \n' + "

    The display CSS property" + " specifies the type of rendering box used for an element.

    \n" + "

    {{cssinfo}}

    \n" '

    Syntax

    \n' '
    '
    -            'display: none;\n'
    -            '
    ' + "display: none;\n" + "" ), - 'slug': 'Web/CSS/display', - 'tags': '"CSS" "CSS Property" "Reference"', - 'title': 'display', - 'toc_depth': Revision.TOC_DEPTH_ALL, + "slug": "Web/CSS/display", + "tags": '"CSS" "CSS Property" "Reference"', + "title": "display", + "toc_depth": Revision.TOC_DEPTH_ALL, } view_data_extra = { # Extra data from view, derived from POST - 'form': 'rev', - 'content': ( + "form": "rev", + "content": ( '

    Summary

    \n' - '

    The display CSS property' - ' specifies the type of rendering box used for an element.

    \n' - '

    {{cssinfo}} and my changes.

    \n' + "

    The display CSS property" + " specifies the type of rendering box used for an element.

    \n" + "

    {{cssinfo}} and my changes.

    \n" '

    Syntax

    \n' '

    Buy my product!

    \n' '
    display: none;
    \n' ), - 'comment': 'Comment', - 'days': '0', - 'hours': '0', - 'minutes': '0', - 'render_max_age': '0', - 'parent_id': '', - 'review_tags': [], + "comment": "Comment", + "days": "0", + "hours": "0", + "minutes": "0", + "render_max_age": "0", + "parent_id": "", + "review_tags": [], } def setup_form( - self, mock_requests, override_original=None, override_data=None, - is_spam=b'false'): + self, + mock_requests, + override_original=None, + override_data=None, + is_spam=b"false", + ): """ Setup a RevisionForm for a POST to edit a page. @@ -385,34 +404,36 @@ def setup_form( * override_data - Add or modify the view data * is_spam - Response from the Akismet check-comment URL """ - revision(save=True, slug='Web') - revision(save=True, slug='Web/CSS') + revision(save=True, slug="Web") + revision(save=True, slug="Web/CSS") original_params = self.original.copy() original_params.update(override_original or {}) previous_revision = revision(save=True, **original_params) data = self.original.copy() - data['current_rev'] = str(previous_revision.id) - del data['slug'] # Not included in edit POST + data["current_rev"] = str(previous_revision.id) + del data["slug"] # Not included in edit POST data.update(self.view_data_extra) data.update(override_data or {}) - request = self.rf.post('/en-US/docs/Web/CSS/display$edit') + request = self.rf.post("/en-US/docs/Web/CSS/display$edit") request.user = self.testuser # The mock request content has to be a byte string if not isinstance(is_spam, bytes): is_spam = is_spam.encode() - mock_requests.post(VERIFY_URL, content=b'valid') + mock_requests.post(VERIFY_URL, content=b"valid") mock_requests.post(CHECK_URL, content=is_spam) section_id = None is_async_submit = False - rev_form = RevisionForm(request=request, - data=data, - is_async_submit=is_async_submit, - section_id=section_id) + rev_form = RevisionForm( + request=request, + data=data, + is_async_submit=is_async_submit, + section_id=section_id, + ) rev_form.instance.document = previous_revision.document return rev_form @@ -425,19 +446,20 @@ def test_standard_edit(self, mock_requests): parameters = rev_form.akismet_parameters() assert sorted(parameters.keys()) == self.akismet_keys_edit expected_content = ( - '

    {{cssinfo}} and my changes.

    \n' + "

    {{cssinfo}} and my changes.

    \n" '

    Buy my product!

    \n' '
    display: none;
    \n' - 'Comment' + "Comment" + ) + assert parameters["comment_content"] == expected_content + assert parameters["comment_type"] == "wiki-revision" + assert parameters["blog"] == "http://testserver/" + assert parameters["blog_lang"] == "en_us" + assert parameters["blog_charset"] == "UTF-8" + assert parameters["REMOTE_ADDR"] == "127.0.0.1" + assert parameters["permalink"] == ( + "http://testserver/en-US/docs/" "Web/CSS/display" ) - assert parameters['comment_content'] == expected_content - assert parameters['comment_type'] == 'wiki-revision' - assert parameters['blog'] == 'http://testserver/' - assert parameters['blog_lang'] == 'en_us' - assert parameters['blog_charset'] == 'UTF-8' - assert parameters['REMOTE_ADDR'] == '127.0.0.1' - assert parameters['permalink'] == ('http://testserver/en-US/docs/' - 'Web/CSS/display') @pytest.mark.spam @requests_mock.mock() @@ -449,19 +471,18 @@ def test_change_tags_edit(self, mock_requests): includes them. """ new_tags = '"CSS" "CSS Property" "Reference" "CSS Positioning"' - rev_form = self.setup_form(mock_requests, - override_data={'tags': new_tags}) + rev_form = self.setup_form(mock_requests, override_data={"tags": new_tags}) assert rev_form.is_valid() parameters = rev_form.akismet_parameters() assert sorted(parameters.keys()) == self.akismet_keys_edit expected_content = ( - '

    {{cssinfo}} and my changes.

    \n' + "

    {{cssinfo}} and my changes.

    \n" '

    Buy my product!

    \n' '
    display: none;
    \n' - 'Comment\n' - 'CSS Positioning' + "Comment\n" + "CSS Positioning" ) - assert parameters['comment_content'] == expected_content + assert parameters["comment_content"] == expected_content @pytest.mark.spam @requests_mock.mock() @@ -472,26 +493,28 @@ def test_legacy_edit(self, mock_requests): Keywords and summary are included in the form if the legacy page includes them. """ - legacy_fields = {'keywords': 'CSS, display', - 'summary': 'CSS property display'} - extra_post_data = {'keywords': 'CSS display, hidden', - 'summary': 'The CSS property display', - 'comment': 'Updated'} - rev_form = self.setup_form(mock_requests, - override_original=legacy_fields, - override_data=extra_post_data) + legacy_fields = {"keywords": "CSS, display", "summary": "CSS property display"} + extra_post_data = { + "keywords": "CSS display, hidden", + "summary": "The CSS property display", + "comment": "Updated", + } + rev_form = self.setup_form( + mock_requests, + override_original=legacy_fields, + override_data=extra_post_data, + ) assert rev_form.is_valid() parameters = rev_form.akismet_parameters() assert sorted(parameters.keys()) == self.akismet_keys_edit expected_content = ( - 'The CSS property display\n' + - '

    {{cssinfo}} and my changes.

    \n' + "The CSS property display\n" + "

    {{cssinfo}} and my changes.

    \n" '

    Buy my product!

    \n' '
    display: none;
    \n' - 'Updated\n' - 'CSS display, hidden' + "Updated\n" + "CSS display, hidden" ) - assert parameters['comment_content'] == expected_content + assert parameters["comment_content"] == expected_content @pytest.mark.spam @requests_mock.mock() @@ -501,21 +524,21 @@ def test_quoteless_tags(self, mock_requests): Tracked in bug 1268511. """ - tags = {'tags': 'CodingScripting, Glossary'} + tags = {"tags": "CodingScripting, Glossary"} rev_form = self.setup_form(mock_requests, override_original=tags) assert rev_form.is_valid() parameters = rev_form.akismet_parameters() assert sorted(parameters.keys()) == self.akismet_keys_edit expected_content = ( - '

    {{cssinfo}} and my changes.

    \n' + "

    {{cssinfo}} and my changes.

    \n" '

    Buy my product!

    \n' '
    display: none;
    \n' - 'Comment\n' - 'CSS\n' - 'CSS Property\n' - 'Reference' + "Comment\n" + "CSS\n" + "CSS Property\n" + "Reference" ) - assert parameters['comment_content'] == expected_content + assert parameters["comment_content"] == expected_content @requests_mock.mock() @pytest.mark.spam @@ -531,22 +554,22 @@ def test_akismet_ham(self, mock_requests): def test_akismet_spam(self, mock_requests): assert DocumentSpamAttempt.objects.count() == 0 assert len(mail.outbox) == 0 - rev_form = self.setup_form(mock_requests, is_spam=b'true') + rev_form = self.setup_form(mock_requests, is_spam=b"true") assert not rev_form.is_valid() - assert rev_form.errors == {'__all__': [rev_form.akismet_error_message]} - admin_path = reverse('admin:wiki_documentspamattempt_changelist') + assert rev_form.errors == {"__all__": [rev_form.akismet_error_message]} + admin_path = reverse("admin:wiki_documentspamattempt_changelist") admin_url = admin_path assert admin_url not in rev_form.akismet_error_message assert DocumentSpamAttempt.objects.count() > 0 attempt = DocumentSpamAttempt.objects.latest() - assert attempt.title == 'display' - assert attempt.slug == 'Web/CSS/display' + assert attempt.title == "display" + assert attempt.slug == "Web/CSS/display" assert attempt.user == self.testuser assert attempt.review == DocumentSpamAttempt.NEEDS_REVIEW assert attempt.data data = json.loads(attempt.data) - assert 'akismet_status_code' not in data + assert "akismet_status_code" not in data # Test that one message has been sent. assert len(mail.outbox) == 1 @@ -558,13 +581,13 @@ def test_akismet_spam(self, mock_requests): @requests_mock.mock() @pytest.mark.spam def test_akismet_spam_moderator_prompt(self, mock_requests): - rev_form = self.setup_form(mock_requests, is_spam='true') - change_perm = Permission.objects.get(codename='change_documentspamattempt') + rev_form = self.setup_form(mock_requests, is_spam="true") + change_perm = Permission.objects.get(codename="change_documentspamattempt") self.testuser.user_permissions.add(change_perm) assert not rev_form.is_valid() - assert rev_form.errors == {'__all__': [rev_form.akismet_error_message]} - admin_path = reverse('admin:wiki_documentspamattempt_changelist') - admin_url = admin_path + '?review__exact=0' + assert rev_form.errors == {"__all__": [rev_form.akismet_error_message]} + admin_path = reverse("admin:wiki_documentspamattempt_changelist") + admin_url = admin_path + "?review__exact=0" assert admin_url in rev_form.akismet_error_message @requests_mock.mock() @@ -572,18 +595,18 @@ def test_akismet_spam_moderator_prompt(self, mock_requests): def test_akismet_error(self, mock_requests): assert DocumentSpamAttempt.objects.count() == 0 assert len(mail.outbox) == 0 - rev_form = self.setup_form(mock_requests, is_spam=b'terrible') + rev_form = self.setup_form(mock_requests, is_spam=b"terrible") assert not rev_form.is_valid() - assert rev_form.errors == {'__all__': [rev_form.akismet_error_message]} + assert rev_form.errors == {"__all__": [rev_form.akismet_error_message]} assert DocumentSpamAttempt.objects.count() > 0 attempt = DocumentSpamAttempt.objects.latest() assert attempt.review == DocumentSpamAttempt.AKISMET_ERROR assert attempt.data data = json.loads(attempt.data) - assert data['akismet_status_code'] == 200 - assert data['akismet_debug_help'] == 'Not provided' - assert data['akismet_response'] == 'terrible' + assert data["akismet_status_code"] == 200 + assert data["akismet_debug_help"] == "Not provided" + assert data["akismet_response"] == "terrible" assert len(mail.outbox) == 1 @@ -592,7 +615,7 @@ def test_akismet_error(self, mock_requests): @override_switch(SPAM_TRAINING_SWITCH, True) def test_akismet_spam_training(self, mock_requests): assert not DocumentSpamAttempt.objects.exists() - rev_form = self.setup_form(mock_requests, is_spam='true') + rev_form = self.setup_form(mock_requests, is_spam="true") assert rev_form.is_valid() assert DocumentSpamAttempt.objects.count() == 1 attempt = DocumentSpamAttempt.objects.get() @@ -604,7 +627,7 @@ def test_akismet_spam_training(self, mock_requests): @override_switch(SPAM_TRAINING_SWITCH, True) def test_akismet_error_training(self, mock_requests): assert not DocumentSpamAttempt.objects.exists() - rev_form = self.setup_form(mock_requests, is_spam='error') + rev_form = self.setup_form(mock_requests, is_spam="error") assert rev_form.is_valid() assert DocumentSpamAttempt.objects.count() == 1 attempt = DocumentSpamAttempt.objects.get() @@ -618,16 +641,16 @@ def test_akismet_parameters_admin_flag(self, mock_requests): rev_form = self.setup_form(mock_requests) assert rev_form.is_valid() parameters = rev_form.akismet_parameters() - assert parameters['user_role'] == 'administrator' + assert parameters["user_role"] == "administrator" @pytest.mark.spam @requests_mock.mock() @override_flag(SPAM_SPAMMER_FLAG, True) def test_akismet_parameters_spammer_flag(self, mock_requests): - rev_form = self.setup_form(mock_requests, is_spam='true') + rev_form = self.setup_form(mock_requests, is_spam="true") assert not rev_form.is_valid() parameters = rev_form.akismet_parameters() - assert parameters['comment_author'] == 'viagra-test-123' + assert parameters["comment_author"] == "viagra-test-123" @pytest.mark.spam @requests_mock.mock() @@ -636,23 +659,23 @@ def test_akismet_parameters_testing_flag(self, mock_requests): rev_form = self.setup_form(mock_requests) assert rev_form.is_valid() parameters = rev_form.akismet_parameters() - assert parameters['is_test'] + assert parameters["is_test"] @pytest.mark.spam @requests_mock.mock() def test_akismet_set_review_flags(self, mock_requests): only_set_review_flags = { - 'content': self.original['content'], - 'comment': '', - 'review_tags': ['editorial', 'technical'] + "content": self.original["content"], + "comment": "", + "review_tags": ["editorial", "technical"], } - rev_form = self.setup_form(mock_requests, - override_data=only_set_review_flags, - is_spam='true') + rev_form = self.setup_form( + mock_requests, override_data=only_set_review_flags, is_spam="true" + ) assert rev_form.is_valid() parameters = rev_form.akismet_parameters() - assert parameters['comment_content'] == '' + assert parameters["comment_content"] == "" assert mock_requests.call_count == 1 # Only verify key called @pytest.mark.spam @@ -666,77 +689,79 @@ def test_akismet_significant_normalized_whitespace(self, mock_requests): """ original = ( '

    Tabs

    \r\n' - '

    \r\n' + "

    \r\n" '\tThe "tab" or tabulator key, was added to typewriters in\r\n' - '\tthe late 19th century, to aid in the typing of tabular data\r\n' - '\tsuch as columns of numbers. In the modern computing era, a\r\n' - '\tdomain-specific language such as CSV or HTML tables\r\n' - '\tshould be used for tabular data. It is an on-going\r\n' - '\teffort to remove the deprecated tab character from\r\n' - '\tsource documents.\r\n' - '

    \r\n') + "\tthe late 19th century, to aid in the typing of tabular data\r\n" + "\tsuch as columns of numbers. In the modern computing era, a\r\n" + "\tdomain-specific language such as CSV or HTML tables\r\n" + "\tshould be used for tabular data. It is an on-going\r\n" + "\teffort to remove the deprecated tab character from\r\n" + "\tsource documents.\r\n" + "

    \r\n" + ) new = ( '

    Tabs

    \r\n' - '

    \r\n' + "

    \r\n" ' The "tab" or tabulator key, was added to typewriters in\n' - ' the late 19th century, to aid in the typing of tabular data\n' - ' such as columns of numbers. In the modern computing era, a\n' - ' domain-specific language such as CSV or HTML tables\n' - ' should be used for tabular data. It is an on-going\n' - ' effort to remove the deprecated tab character from\n' - ' source documents.\r\n' - '

    \n') - rev_form = self.setup_form(mock_requests, - override_original={'content': original}, - override_data={'content': new}) + " the late 19th century, to aid in the typing of tabular data\n" + " such as columns of numbers. In the modern computing era, a\n" + " domain-specific language such as CSV or HTML tables\n" + " should be used for tabular data. It is an on-going\n" + " effort to remove the deprecated tab character from\n" + " source documents.\r\n" + "

    \n" + ) + rev_form = self.setup_form( + mock_requests, + override_original={"content": original}, + override_data={"content": new}, + ) assert rev_form.is_valid() parameters = rev_form.akismet_parameters() # Akismet sees a content change due to the whitespace - assert parameters['comment_content'] != '' + assert parameters["comment_content"] != "" class RevisionFormCreateTests(RevisionFormViewTests): """Test RevisionForm as used in create view.""" view_data = { # Data passed by view, derived from POST - 'comment': 'Initial version', - 'content': ( + "comment": "Initial version", + "content": ( '

    Summary

    \r\n' - '

    Web accessibility is removing barriers that prevent' - ' interaction with or access to website.

    \r\n' + "

    Web accessibility is removing barriers that prevent" + " interaction with or access to website.

    \r\n" ), - 'locale': 'en-US', # Added in view from request.LANGUAGE_CODE - 'review_tags': ['technical', 'editorial'], - 'slug': 'Accessibility', - 'tags': '"Accessibility" "Web Development"', - 'title': 'Accessibility', - 'toc_depth': Revision.TOC_DEPTH_ALL, + "locale": "en-US", # Added in view from request.LANGUAGE_CODE + "review_tags": ["technical", "editorial"], + "slug": "Accessibility", + "tags": '"Accessibility" "Web Development"', + "title": "Accessibility", + "toc_depth": Revision.TOC_DEPTH_ALL, } - def setup_form(self, mock_requests, is_spam=b'false'): + def setup_form(self, mock_requests, is_spam=b"false"): """ Setup a RevisionForm for a POST to create a new page. Parameters: * mock_requests - Mockable requests for Akismet checks """ - revision(save=True, slug='Web') - parent = revision(save=True, slug='Web/Guide') + revision(save=True, slug="Web") + parent = revision(save=True, slug="Web/Guide") data = self.view_data.copy() - data['parent_topic'] = str(parent.id) + data["parent_topic"] = str(parent.id) - request = self.rf.post('/en-US/docs/new') + request = self.rf.post("/en-US/docs/new") request.user = self.testuser # In the view, the form data's locale is set from the request - request.LANGUAGE_CODE = data['locale'] + request.LANGUAGE_CODE = data["locale"] - mock_requests.post(VERIFY_URL, content=b'valid') + mock_requests.post(VERIFY_URL, content=b"valid") mock_requests.post(CHECK_URL, content=is_spam) - parent_slug = 'Web/Guide' - rev_form = RevisionForm(request=request, - data=data, - parent_slug=parent_slug) + parent_slug = "Web/Guide" + rev_form = RevisionForm(request=request, data=data, parent_slug=parent_slug) return rev_form @pytest.mark.spam @@ -747,45 +772,45 @@ def test_standard_new(self, mock_requests): assert rev_form.is_valid(), rev_form.errors parameters = rev_form.akismet_parameters() assert sorted(parameters.keys()) == self.akismet_keys - assert parameters['blog'] == 'http://testserver/' - assert parameters['blog_charset'] == 'UTF-8' - assert parameters['blog_lang'] == 'en_us' - assert parameters['comment_author'] == 'Test User' - assert parameters['comment_author_email'] == self.testuser.email + assert parameters["blog"] == "http://testserver/" + assert parameters["blog_charset"] == "UTF-8" + assert parameters["blog_lang"] == "en_us" + assert parameters["comment_author"] == "Test User" + assert parameters["comment_author_email"] == self.testuser.email expected_content = ( - 'Accessibility\n' - 'Web/Guide/Accessibility\n' + "Accessibility\n" + "Web/Guide/Accessibility\n" '

    Summary

    \n' - '

    Web accessibility is removing barriers that prevent' - ' interaction with or access to website.

    \n' - 'Initial version\n' - 'Accessibility\n' - 'Web Development' + "

    Web accessibility is removing barriers that prevent" + " interaction with or access to website.

    \n" + "Initial version\n" + "Accessibility\n" + "Web Development" ) - assert parameters['comment_content'] == expected_content - assert parameters['comment_type'] == 'wiki-revision' - assert parameters['referrer'] == '' - assert parameters['user_agent'] == '' - assert parameters['user_ip'] == '127.0.0.1' + assert parameters["comment_content"] == expected_content + assert parameters["comment_type"] == "wiki-revision" + assert parameters["referrer"] == "" + assert parameters["user_agent"] == "" + assert parameters["user_ip"] == "127.0.0.1" @requests_mock.mock() @pytest.mark.spam def test_akismet_spam(self, mock_requests): assert DocumentSpamAttempt.objects.count() == 0 assert len(mail.outbox) == 0 - rev_form = self.setup_form(mock_requests, is_spam=b'true') + rev_form = self.setup_form(mock_requests, is_spam=b"true") assert not rev_form.is_valid() - assert rev_form.errors == {'__all__': [rev_form.akismet_error_message]} + assert rev_form.errors == {"__all__": [rev_form.akismet_error_message]} assert DocumentSpamAttempt.objects.count() > 0 attempt = DocumentSpamAttempt.objects.latest() - assert attempt.title == 'Accessibility' - assert attempt.slug == 'Web/Guide/Accessibility' + assert attempt.title == "Accessibility" + assert attempt.slug == "Web/Guide/Accessibility" assert attempt.user == self.testuser assert attempt.review == DocumentSpamAttempt.NEEDS_REVIEW assert attempt.data data = json.loads(attempt.data) - assert 'akismet_status_code' not in data + assert "akismet_status_code" not in data # Test that one message has been sent. assert len(mail.outbox) == 1 @@ -799,33 +824,33 @@ class RevisionFormNewTranslationTests(RevisionFormViewTests): """Test RevisionForm as used to create a page in translate view.""" original = { # Default attributes of original English page - 'content': ( + "content": ( '

    Summary

    \n' - '

    HyperText Markup Language (HTML) is the' - ' core language of nearly all Web content.

    \n' + "

    HyperText Markup Language (HTML) is the" + " core language of nearly all Web content.

    \n" ), - 'slug': 'Web/Guide/HTML', - 'tags': '"HTML" "Landing" "Web"', - 'title': 'HTML developer guide', - 'toc_depth': Revision.TOC_DEPTH_ALL, + "slug": "Web/Guide/HTML", + "tags": '"HTML" "Landing" "Web"', + "title": "HTML developer guide", + "toc_depth": Revision.TOC_DEPTH_ALL, } view_data = { # Data passed by view, derived from POST - 'comment': 'Traduction initiale', - 'content': ( + "comment": "Traduction initiale", + "content": ( '

    Summary

    \n' - '

    HyperText Markup Language (HTML), ou' - ' langage de balisage hypertexte, est le langage au cœur' - ' de presque tout contenu Web.

    \n' + "

    HyperText Markup Language (HTML), ou" + " langage de balisage hypertexte, est le langage au cœur" + " de presque tout contenu Web.

    \n" ), - 'current_rev': '', - 'form': 'both', - 'locale': 'fr', # Added in view from request.GET to_locale - 'localization_tags': ['inprogress'], - 'slug': 'HTML', - 'tags': '"HTML" "Landing" "Web"', - 'title': 'Guide de développement HTML', - 'toc_depth': Revision.TOC_DEPTH_ALL, + "current_rev": "", + "form": "both", + "locale": "fr", # Added in view from request.GET to_locale + "localization_tags": ["inprogress"], + "slug": "HTML", + "tags": '"HTML" "Landing" "Web"', + "title": "Guide de développement HTML", + "toc_depth": Revision.TOC_DEPTH_ALL, } def setup_form(self, mock_requests): @@ -835,44 +860,42 @@ def setup_form(self, mock_requests): Parameters: * mock_requests - Mockable requests for Akismet checks """ - revision(save=True, slug='Web') - revision(save=True, slug='Web/Guide') + revision(save=True, slug="Web") + revision(save=True, slug="Web/Guide") original_data = self.original.copy() english_rev = revision(save=True, **original_data) - fr_web_doc = document(save=True, slug='Web', locale='fr') - revision(save=True, slug='Web', document=fr_web_doc) - fr_guide_doc = document(save=True, slug='Web/Guide', locale='fr') - revision(save=True, slug='Web/Guide', document=fr_guide_doc) - fr_html_doc = document(save=True, slug='Web/Guide/HTML', locale='fr', - parent=english_rev.document) + fr_web_doc = document(save=True, slug="Web", locale="fr") + revision(save=True, slug="Web", document=fr_web_doc) + fr_guide_doc = document(save=True, slug="Web/Guide", locale="fr") + revision(save=True, slug="Web/Guide", document=fr_guide_doc) + fr_html_doc = document( + save=True, slug="Web/Guide/HTML", locale="fr", parent=english_rev.document + ) initial = { - 'based_on': english_rev.id, - 'comment': '', - 'toc_depth': english_rev.toc_depth, - 'localization_tags': ['inprogress'], - 'content': english_rev.content, # In view, includes cleaning + "based_on": english_rev.id, + "comment": "", + "toc_depth": english_rev.toc_depth, + "localization_tags": ["inprogress"], + "content": english_rev.content, # In view, includes cleaning } - request = self.rf.post('/en-US/docs/Web/Guide/HTML$translate') + request = self.rf.post("/en-US/docs/Web/Guide/HTML$translate") request.user = self.testuser - mock_requests.post(VERIFY_URL, content=b'valid') - mock_requests.post(CHECK_URL, content=b'false') + mock_requests.post(VERIFY_URL, content=b"valid") + mock_requests.post(CHECK_URL, content=b"false") - parent_slug = 'Web/Guide' - rev_form1 = RevisionForm(request=request, - instance=None, - initial=initial, - parent_slug=parent_slug) + parent_slug = "Web/Guide" + rev_form1 = RevisionForm( + request=request, instance=None, initial=initial, parent_slug=parent_slug + ) assert rev_form1 data = self.view_data.copy() - data['based_on'] = str(english_rev.id) - rev_form = RevisionForm(request=request, - data=data, - parent_slug=parent_slug) + data["based_on"] = str(english_rev.id) + rev_form = RevisionForm(request=request, data=data, parent_slug=parent_slug) rev_form.instance.document = fr_html_doc return rev_form @@ -884,63 +907,63 @@ def test_new_translation(self, mock_requests): assert rev_form.is_valid() parameters = rev_form.akismet_parameters() assert sorted(parameters.keys()) == self.akismet_keys - assert parameters['blog_lang'] == 'fr, en_us' + assert parameters["blog_lang"] == "fr, en_us" expected_content = ( - 'Guide de développement HTML\n' - '

    HyperText Markup Language (HTML), ou' - ' langage de balisage hypertexte, est le langage au cœur' - ' de presque tout contenu Web.

    \n' - 'Traduction initiale' + "Guide de développement HTML\n" + "

    HyperText Markup Language (HTML), ou" + " langage de balisage hypertexte, est le langage au cœur" + " de presque tout contenu Web.

    \n" + "Traduction initiale" ) - assert parameters['comment_content'] == expected_content + assert parameters["comment_content"] == expected_content class RevisionFormEditTranslationTests(RevisionFormViewTests): """Test RevisionForm as used to create a page in translate view.""" en_original = { # Default attributes of original English page - 'content': ( + "content": ( '

    Summary

    \n' - '

    HyperText Markup Language (HTML) is the' - ' core language of nearly all Web content.

    \n' + "

    HyperText Markup Language (HTML) is the" + " core language of nearly all Web content.

    \n" ), - 'slug': 'Web/Guide/HTML', - 'tags': '"HTML" "Landing" "Web"', - 'title': 'HTML developer guide', - 'toc_depth': Revision.TOC_DEPTH_ALL, + "slug": "Web/Guide/HTML", + "tags": '"HTML" "Landing" "Web"', + "title": "HTML developer guide", + "toc_depth": Revision.TOC_DEPTH_ALL, } fr_original = { # Default attributes of original French page - 'content': ( + "content": ( '

    Summary

    \n' - '

    HyperText Markup Language (HTML), ou' - ' langage de balisage hypertexte, est le langage au cœur' - ' de presque tout contenu Web.

    \n' + "

    HyperText Markup Language (HTML), ou" + " langage de balisage hypertexte, est le langage au cœur" + " de presque tout contenu Web.

    \n" ), - 'slug': 'Web/Guide/HTML', - 'tags': '"HTML" "Landing"', - 'title': 'Guide de développement HTML', - 'toc_depth': Revision.TOC_DEPTH_ALL, + "slug": "Web/Guide/HTML", + "tags": '"HTML" "Landing"', + "title": "Guide de développement HTML", + "toc_depth": Revision.TOC_DEPTH_ALL, } view_data = { # Data passed by view, derived from POST - 'comment': 'Traduction initiale terminée', - 'content': ( + "comment": "Traduction initiale terminée", + "content": ( '

    Summary

    \n' - '

    HyperText Markup Language (HTML), ou' - ' langage de balisage hypertexte, est le langage au cœur' - ' de presque tout contenu Web.

    \n' - '

    La majorité de ce que vous voyez dans votre navigateur est' - ' décrit en utilisant HTML.

    ' + "

    HyperText Markup Language (HTML), ou" + " langage de balisage hypertexte, est le langage au cœur" + " de presque tout contenu Web.

    \n" + "

    La majorité de ce que vous voyez dans votre navigateur est" + " décrit en utilisant HTML.

    " ), - 'current_rev': '', - 'form': 'both', - 'locale': 'fr', # Added in view from request.GET to_locale - 'localization_tags': ['inprogress'], - 'slug': 'HTML', - 'tags': '"HTML" "Landing" "Web"', - 'title': 'Guide de développement HTML', - 'toc_depth': Revision.TOC_DEPTH_ALL, + "current_rev": "", + "form": "both", + "locale": "fr", # Added in view from request.GET to_locale + "localization_tags": ["inprogress"], + "slug": "HTML", + "tags": '"HTML" "Landing" "Web"', + "title": "Guide de développement HTML", + "toc_depth": Revision.TOC_DEPTH_ALL, } def setup_forms(self, mock_requests): @@ -953,40 +976,37 @@ def setup_forms(self, mock_requests): Parameters: * mock_requests - Mockable requests for Akismet checks """ - revision(save=True, slug='Web') - revision(save=True, slug='Web/Guide') + revision(save=True, slug="Web") + revision(save=True, slug="Web/Guide") en_rev = revision(save=True, **self.en_original) - fr_web_doc = document(save=True, slug='Web', locale='fr') - revision(save=True, slug='Web', document=fr_web_doc) - fr_guide_doc = document(save=True, slug='Web/Guide', locale='fr') - revision(save=True, slug='Web/Guide', document=fr_guide_doc) - fr_html_doc = document(save=True, slug='Web/Guide/HTML', locale='fr', - parent=en_rev.document) + fr_web_doc = document(save=True, slug="Web", locale="fr") + revision(save=True, slug="Web", document=fr_web_doc) + fr_guide_doc = document(save=True, slug="Web/Guide", locale="fr") + revision(save=True, slug="Web/Guide", document=fr_guide_doc) + fr_html_doc = document( + save=True, slug="Web/Guide/HTML", locale="fr", parent=en_rev.document + ) revision(save=True, document=fr_html_doc, **self.fr_original) - request = self.rf.post('/fr/docs/Web/Guide/HTML') + request = self.rf.post("/fr/docs/Web/Guide/HTML") request.user = self.testuser - mock_requests.post(VERIFY_URL, content=b'valid') - mock_requests.post(CHECK_URL, content=b'false') + mock_requests.post(VERIFY_URL, content=b"valid") + mock_requests.post(CHECK_URL, content=b"false") # Form #1 - Document validation data = self.view_data.copy() - data['based_on'] = str(en_rev.id) - data['parent_id'] = str(en_rev.document.id) - parent_slug = 'Web/Guide' - rev_form1 = RevisionForm(request=request, - data=data, - parent_slug=parent_slug) + data["based_on"] = str(en_rev.id) + data["parent_id"] = str(en_rev.document.id) + parent_slug = "Web/Guide" + rev_form1 = RevisionForm(request=request, data=data, parent_slug=parent_slug) # Form #2 - Revision validation and saving data = self.view_data.copy() - data['based_on'] = str(en_rev.id) - data['parent_id'] = str(en_rev.document.id) - rev_form2 = RevisionForm(request=request, - data=data, - parent_slug=parent_slug) + data["based_on"] = str(en_rev.id) + data["parent_id"] = str(en_rev.document.id) + rev_form2 = RevisionForm(request=request, data=data, parent_slug=parent_slug) rev_form2.instance.document = fr_html_doc return rev_form1, rev_form2 @@ -999,20 +1019,21 @@ def test_edit_translation(self, mock_requests): assert rev_form2.is_valid(), rev_form2.errors parameters = rev_form2.akismet_parameters() assert sorted(parameters.keys()) == self.akismet_keys_edit - assert parameters['blog_lang'] == 'fr, en_us' + assert parameters["blog_lang"] == "fr, en_us" expected_content = ( - '

    La majorité de ce que vous voyez dans votre navigateur est' - ' décrit en utilisant HTML.

    \n' - 'Traduction initiale terminée\n' - 'Web' + "

    La majorité de ce que vous voyez dans votre navigateur est" + " décrit en utilisant HTML.

    \n" + "Traduction initiale terminée\n" + "Web" + ) + assert parameters["comment_content"] == expected_content + assert parameters["permalink"] == ( + "http://testserver/fr/docs/" "Web/Guide/HTML" ) - assert parameters['comment_content'] == expected_content - assert parameters['permalink'] == ('http://testserver/fr/docs/' - 'Web/Guide/HTML') class TreeMoveFormTests(UserTestCase): - fixtures = UserTestCase.fixtures + ['wiki/documents.json'] + fixtures = UserTestCase.fixtures + ["wiki/documents.json"] def test_form_properly_strips_leading_cruft(self): """ @@ -1020,25 +1041,27 @@ def test_form_properly_strips_leading_cruft(self): are removed if included """ comparisons = [ - ['/somedoc', 'somedoc'], # leading slash - ['/en-US/docs/mynewplace', 'mynewplace'], # locale and docs - ['/docs/one', 'one'], # leading docs - ['docs/one', 'one'], # leading docs without slash - ['fr/docs/one', 'one'], # foreign locale with docs - ['docs/article-title/docs', 'article-title/docs'], # docs with later docs - ['/en-US/docs/something/', 'something'] # trailing slash + ["/somedoc", "somedoc"], # leading slash + ["/en-US/docs/mynewplace", "mynewplace"], # locale and docs + ["/docs/one", "one"], # leading docs + ["docs/one", "one"], # leading docs without slash + ["fr/docs/one", "one"], # foreign locale with docs + ["docs/article-title/docs", "article-title/docs"], # docs with later docs + ["/en-US/docs/something/", "something"], # trailing slash ] for comparison in comparisons: - form = TreeMoveForm({'locale': 'en-US', 'title': 'Article', - 'slug': comparison[0]}) + form = TreeMoveForm( + {"locale": "en-US", "title": "Article", "slug": comparison[0]} + ) form.is_valid() - self.assertEqual(comparison[1], form.cleaned_data['slug']) + self.assertEqual(comparison[1], form.cleaned_data["slug"]) def test_form_enforces_parent_doc_to_exist(self): - form = TreeMoveForm({'locale': 'en-US', 'title': 'Article', - 'slug': 'nothing/article'}) + form = TreeMoveForm( + {"locale": "en-US", "title": "Article", "slug": "nothing/article"} + ) form.is_valid() self.assertTrue(form.errors) - self.assertIn('Parent', form.errors.as_text()) - self.assertIn('does not exist', form.errors.as_text()) + self.assertIn("Parent", form.errors.as_text()) + self.assertIn("does not exist", form.errors.as_text()) diff --git a/kuma/wiki/tests/test_helpers.py b/kuma/wiki/tests/test_helpers.py index b9edf21ff84..a8183c571d2 100644 --- a/kuma/wiki/tests/test_helpers.py +++ b/kuma/wiki/tests/test_helpers.py @@ -1,5 +1,3 @@ - - from datetime import datetime import pytest @@ -7,73 +5,77 @@ from pyquery import PyQuery as pq from ..models import Document, Revision -from ..templatetags.jinja_helpers import (absolutify, - include_svg, - revisions_unified_diff, - selector_content_find, tojson, - wiki_url) +from ..templatetags.jinja_helpers import ( + absolutify, + include_svg, + revisions_unified_diff, + selector_content_find, + tojson, + wiki_url, +) def test_tojson(): """tojson converts dicts to JSON objects with escaping.""" - output = tojson({'title': ''}) - expected = ('{"title": "<script>alert("Hi!")' - '</script>"}') + output = tojson({"title": ''}) + expected = '{"title": "<script>alert("Hi!")' '</script>"}' assert output == expected @pytest.mark.parametrize( - 'path,abspath', - (('', 'https://testserver/'), - ('/', 'https://testserver/'), - ('//', 'https://testserver/'), - ('/foo/bar', 'https://testserver/foo/bar'), - ('http://domain.com', 'http://domain.com'), - ('/woo?var=value', 'https://testserver/woo?var=value'), - ('/woo?var=value#fragment', 'https://testserver/woo?var=value#fragment'), - )) + "path,abspath", + ( + ("", "https://testserver/"), + ("/", "https://testserver/"), + ("//", "https://testserver/"), + ("/foo/bar", "https://testserver/foo/bar"), + ("http://domain.com", "http://domain.com"), + ("/woo?var=value", "https://testserver/woo?var=value"), + ("/woo?var=value#fragment", "https://testserver/woo?var=value#fragment"), + ), +) def test_absolutify(settings, path, abspath): """absolutify adds the current site to paths without domains.""" - settings.SITE_URL = 'https://testserver' + settings.SITE_URL = "https://testserver" assert absolutify(path) == abspath def test_absolutify_dev(settings): """absolutify uses http in development.""" - settings.SITE_URL = 'http://localhost:8000' - assert absolutify('') == 'http://localhost:8000/' + settings.SITE_URL = "http://localhost:8000" + assert absolutify("") == "http://localhost:8000/" def test_include_svg_invalid_path(): """An invalid SVG path raises an exception.""" with pytest.raises(TemplateDoesNotExist): - include_svg('invalid.svg') + include_svg("invalid.svg") def test_include_svg_no_title(): """If the title is not given, the SVG title is not changed.""" - no_title = include_svg('includes/icons/social/twitter.svg') - svg = pq(no_title, namespaces={'svg': 'http://www.w3.org/2000/svg'}) - svg_title = svg('svg|title') - assert svg_title.text() == 'Twitter' + no_title = include_svg("includes/icons/social/twitter.svg") + svg = pq(no_title, namespaces={"svg": "http://www.w3.org/2000/svg"}) + svg_title = svg("svg|title") + assert svg_title.text() == "Twitter" -@pytest.mark.parametrize('title', ('New Title', 'Nuevo Título')) +@pytest.mark.parametrize("title", ("New Title", "Nuevo Título")) def test_include_svg_replace_title(title): """The SVG title can be replaced.""" - new_title = include_svg('includes/icons/social/twitter.svg', title) - svg = pq(new_title, namespaces={'svg': 'http://www.w3.org/2000/svg'}) - svg_title = svg('svg|title') + new_title = include_svg("includes/icons/social/twitter.svg", title) + svg = pq(new_title, namespaces={"svg": "http://www.w3.org/2000/svg"}) + svg_title = svg("svg|title") assert svg_title.text() == title def test_include_svg_add_title_title_id(): """The SVG title and id attribute can be added.""" - title, title_id = 'New Title', 'title-id' - new_svg = include_svg('includes/icons/social/twitter.svg', title, title_id) - new_svg = pq(new_svg, namespaces={'svg': 'http://www.w3.org/2000/svg'}) - svg_title = new_svg('svg|title') - svg_title_id = new_svg('svg|title').attr['id'] + title, title_id = "New Title", "title-id" + new_svg = include_svg("includes/icons/social/twitter.svg", title, title_id) + new_svg = pq(new_svg, namespaces={"svg": "http://www.w3.org/2000/svg"}) + svg_title = new_svg("svg|title") + svg_title_id = new_svg("svg|title").attr["id"] assert svg_title.text() == title assert svg_title_id == title_id @@ -86,25 +88,25 @@ def test_revisions_unified_diff_none(root_doc): def test_revisions_unified_diff_non_ascii(wiki_user): """Documents with non-ASCII titles do not have Unicode errors in diffs.""" - title1 = 'Gänsefüßchen' - doc1 = Document.objects.create( - locale='en-US', slug=title1, title=title1) + title1 = "Gänsefüßchen" + doc1 = Document.objects.create(locale="en-US", slug=title1, title=title1) rev1 = Revision.objects.create( document=doc1, creator=wiki_user, - content='

    %s started...

    ' % title1, + content="

    %s started...

    " % title1, title=title1, - created=datetime(2018, 11, 21, 18, 39)) + created=datetime(2018, 11, 21, 18, 39), + ) - title2 = 'Außendienstüberwachlösung' - doc2 = Document.objects.create( - locale='en-US', slug=title2, title=title2) + title2 = "Außendienstüberwachlösung" + doc2 = Document.objects.create(locale="en-US", slug=title2, title=title2) rev2 = Revision.objects.create( document=doc2, creator=wiki_user, - content='

    %s started...

    ' % title2, + content="

    %s started...

    " % title2, title=title1, - created=datetime(2018, 11, 21, 18, 41)) + created=datetime(2018, 11, 21, 18, 41), + ) revisions_unified_diff(rev1, rev2) # No UnicodeEncodeError @@ -112,23 +114,28 @@ def test_revisions_unified_diff_non_ascii(wiki_user): def test_selector_content_find_not_found_returns_empty_string(root_doc): """When the ID is not in the content, return an empty string.""" root_doc.rendered_html = root_doc.current_revision.content - content = selector_content_find(root_doc, 'summary') - assert content == '' + content = selector_content_find(root_doc, "summary") + assert content == "" def test_selector_content_find_bad_selector_returns_empty_string(root_doc): """When the ID is invalid, return an empty string.""" root_doc.rendered_html = root_doc.current_revision.content - content = selector_content_find(root_doc, '.') - assert content == '' + content = selector_content_find(root_doc, ".") + assert content == "" @pytest.mark.parametrize( - "path, expected", ( - ('MDN/Getting_started', '/en-US/docs/MDN/Getting_started'), - ('MDN/Getting_started#Option_1_I_like_words', - '/en-US/docs/MDN/Getting_started#Option_1_I_like_words'), - ), ids=('simple', 'fragment')) + "path, expected", + ( + ("MDN/Getting_started", "/en-US/docs/MDN/Getting_started"), + ( + "MDN/Getting_started#Option_1_I_like_words", + "/en-US/docs/MDN/Getting_started#Option_1_I_like_words", + ), + ), + ids=("simple", "fragment"), +) def test_wiki_url(path, expected): """Test wiki_url, without client languages.""" out = wiki_url(path) diff --git a/kuma/wiki/tests/test_jobs.py b/kuma/wiki/tests/test_jobs.py index 5d41e9490f7..01e253d85b3 100644 --- a/kuma/wiki/tests/test_jobs.py +++ b/kuma/wiki/tests/test_jobs.py @@ -1,5 +1,3 @@ - - from datetime import datetime import pytest @@ -9,13 +7,14 @@ @pytest.mark.parametrize("mode", ["maintenance-mode", "normal-mode"]) -def test_contributors(db, settings, wiki_user_3, - root_doc_with_mixed_contributors, mode): +def test_contributors( + db, settings, wiki_user_3, root_doc_with_mixed_contributors, mode +): """ Tests basic operation, ordering, caching, and handling of banned and inactive contributors. """ - settings.MAINTENANCE_MODE = (mode == "maintenance-mode") + settings.MAINTENANCE_MODE = mode == "maintenance-mode" fixture = root_doc_with_mixed_contributors root_doc = fixture.doc @@ -31,7 +30,7 @@ def test_contributors(db, settings, wiki_user_3, valid_contrib_ids = [user.pk for user in fixture.contributors.valid] # Banned and inactive contributors should not be included. - assert [c['id'] for c in contributors] == valid_contrib_ids + assert [c["id"] for c in contributors] == valid_contrib_ids banned_user = fixture.contributors.banned.user @@ -41,21 +40,21 @@ def test_contributors(db, settings, wiki_user_3, # The freshly un-banned user is now among the contributors because the # cache has been invalidated. contributors = job.get(root_doc.pk) - got = set(c['id'] for c in contributors) + got = set(c["id"] for c in contributors) assert banned_user.pk in got # Another revision should invalidate the job's cache. root_doc.current_revision = Revision.objects.create( document=root_doc, creator=wiki_user_3, - content='

    The root document re-envisioned.

    ', - comment='Done with the previous version.', - created=datetime(2017, 4, 24, 12, 35) + content="

    The root document re-envisioned.

    ", + comment="Done with the previous version.", + created=datetime(2017, 4, 24, 12, 35), ) root_doc.save() # The new contributor shows up and is first, followed # by the freshly un-banned user, and then the rest. contributors = job.get(root_doc.pk) - got = set(c['id'] for c in contributors) + got = set(c["id"] for c in contributors) assert got == set([wiki_user_3.pk, banned_user.pk] + valid_contrib_ids) diff --git a/kuma/wiki/tests/test_kumascript.py b/kuma/wiki/tests/test_kumascript.py index 66e80d354ba..0b40d56f3a3 100644 --- a/kuma/wiki/tests/test_kumascript.py +++ b/kuma/wiki/tests/test_kumascript.py @@ -1,5 +1,3 @@ - - import base64 import json from unittest import mock @@ -9,12 +7,16 @@ import requests_mock from elasticsearch import TransportError from elasticsearch_dsl.connections import connections -from requests.exceptions import (ConnectionError, ContentDecodingError, - ReadTimeout, TooManyRedirects) +from requests.exceptions import ( + ConnectionError, + ContentDecodingError, + ReadTimeout, + TooManyRedirects, +) from . import WikiTestCase from .. import kumascript -from .. constants import KUMASCRIPT_BASE_URL +from ..constants import KUMASCRIPT_BASE_URL @pytest.yield_fixture @@ -26,72 +28,69 @@ def mock_es_client(request): Based on test fixture from elasticsearch_dsl """ client = mock.Mock() - connections.add_connection('default', client) + connections.add_connection("default", client) yield client connections._conn = {} connections._kwargs = {} class KumascriptClientTests(WikiTestCase): - def test_env_vars(self): """Exercise building of env var headers for kumascript""" headers = dict() env_vars = dict( - path='/foo/test-slug', - title='Test title', - slug='test-slug', - locale='de', - tags=['foo', 'bar', 'baz'] + path="/foo/test-slug", + title="Test title", + slug="test-slug", + locale="de", + tags=["foo", "bar", "baz"], ) kumascript.add_env_headers(headers, env_vars) - pfx = 'x-kumascript-env-' + pfx = "x-kumascript-env-" result_vars = dict( - (k[len(pfx):], json.loads(base64.b64decode(v))) + (k[len(pfx) :], json.loads(base64.b64decode(v))) for k, v in headers.items() - if k.startswith(pfx)) + if k.startswith(pfx) + ) # Ensure the env vars intended for kumascript match expected values. - for n in ('title', 'slug', 'locale', 'path'): + for n in ("title", "slug", "locale", "path"): assert env_vars[n] == result_vars[n] - assert {'foo', 'bar', 'baz'} == set(result_vars['tags']) + assert {"foo", "bar", "baz"} == set(result_vars["tags"]) def test_macro_sources(mock_requests): """When KumaScript returns macros, the sources are populated.""" - macros_url = urljoin(KUMASCRIPT_BASE_URL, 'macros/') + macros_url = urljoin(KUMASCRIPT_BASE_URL, "macros/") response = { - 'can_list_macros': True, - 'loader': 'FileLoader', - 'macros': [{ - 'filename': 'A11yRoleQuicklinks.ejs', 'name': 'A11yRoleQuicklinks' - }, { - 'filename': 'APIFeatureList.ejs', 'name': 'APIFeatureList' - }, { - # Normal form D, common on OSX - 'filename': 'traduccio\u0301n.ejs', 'name': 'traduccio\u0301n' - }] + "can_list_macros": True, + "loader": "FileLoader", + "macros": [ + {"filename": "A11yRoleQuicklinks.ejs", "name": "A11yRoleQuicklinks"}, + {"filename": "APIFeatureList.ejs", "name": "APIFeatureList"}, + { + # Normal form D, common on OSX + "filename": "traduccio\u0301n.ejs", + "name": "traduccio\u0301n", + }, + ], } mock_requests.get(macros_url, json=response) macros = kumascript.macro_sources() expected = { - 'A11yRoleQuicklinks': 'A11yRoleQuicklinks.ejs', - 'APIFeatureList': 'APIFeatureList.ejs', + "A11yRoleQuicklinks": "A11yRoleQuicklinks.ejs", + "APIFeatureList": "APIFeatureList.ejs", # Normal form C, used on GitHub, ElasticSearch - 'traducci\xf3n': 'traducci\xf3n.ejs', + "traducci\xf3n": "traducci\xf3n.ejs", } assert macros == expected def test_macro_sources_empty_macro_list(mock_requests): """When KumaScript can't return macros, the sources are empty.""" - macros_url = urljoin(KUMASCRIPT_BASE_URL, 'macros/') - response = { - 'can_list_macros': False, - 'loader': 'HTTPLoader', - 'macros': [] - } + macros_url = urljoin(KUMASCRIPT_BASE_URL, "macros/") + response = {"can_list_macros": False, "loader": "HTTPLoader", "macros": []} mock_requests.get(macros_url, json=response) macros = kumascript.macro_sources() assert macros == {} @@ -99,8 +98,8 @@ def test_macro_sources_empty_macro_list(mock_requests): def test_macro_sources_error(mock_requests): """When KumaScript raises an error, the sources are empty.""" - macros_url = urljoin(KUMASCRIPT_BASE_URL, 'macros/') - mock_requests.get(macros_url, status_code=404, text='Cannot GET /macros') + macros_url = urljoin(KUMASCRIPT_BASE_URL, "macros/") + mock_requests.get(macros_url, status_code=404, text="Cannot GET /macros") macros = kumascript.macro_sources() assert macros == {} @@ -108,186 +107,174 @@ def test_macro_sources_error(mock_requests): def test_macro_page_count(db, mock_es_client): """macro_page_count returns macro usage across all locales by default.""" mock_es_client.search.return_value = { - '_shards': {'failed': 0, 'skiped': 0, 'successful': 2, 'total': 2}, - 'aggregations': {'usage': { - 'doc_count_error_upper_bound': 0, - 'sum_other_doc_count': 0, - 'buckets': [ - {'key': 'a11yrolequicklinks', 'doc_count': 200}, - {'key': 'othermacro', 'doc_count': 50}, - ]}}, - 'hits': {'hits': [], 'max_score': 0.0, 'total': 45556}, - 'timed_out': False, - 'took': 18 + "_shards": {"failed": 0, "skiped": 0, "successful": 2, "total": 2}, + "aggregations": { + "usage": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + {"key": "a11yrolequicklinks", "doc_count": 200}, + {"key": "othermacro", "doc_count": 50}, + ], + } + }, + "hits": {"hits": [], "max_score": 0.0, "total": 45556}, + "timed_out": False, + "took": 18, } macros = kumascript.macro_page_count() es_json = { - 'size': 0, - 'aggs': {'usage': {'terms': { - 'field': 'kumascript_macros', - 'size': 2000} - }} + "size": 0, + "aggs": {"usage": {"terms": {"field": "kumascript_macros", "size": 2000}}}, } mock_es_client.search.assert_called_once_with( - body=es_json, - doc_type=['wiki_document'], - index=['mdn-main_index']) - assert macros == {'a11yrolequicklinks': 200, 'othermacro': 50} + body=es_json, doc_type=["wiki_document"], index=["mdn-main_index"] + ) + assert macros == {"a11yrolequicklinks": 200, "othermacro": 50} def test_macro_page_count_en(db, mock_es_client): """macro_page_count('en-US') returns macro usage in the en-US locale.""" mock_es_client.search.return_value = { - '_shards': {'failed': 0, 'skipped': 0, 'successful': 2, 'total': 2}, - 'aggregations': {'usage': { - 'doc_count_error_upper_bound': 0, - 'sum_other_doc_count': 0, - 'buckets': [ - {'key': 'a11yrolequicklinks', 'doc_count': 100}, - {'key': 'othermacro', 'doc_count': 30}, - ]}}, - 'hits': {'hits': [], 'max_score': 0.0, 'total': 45556}, - 'timed_out': False, - 'took': 18 + "_shards": {"failed": 0, "skipped": 0, "successful": 2, "total": 2}, + "aggregations": { + "usage": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + {"key": "a11yrolequicklinks", "doc_count": 100}, + {"key": "othermacro", "doc_count": 30}, + ], + } + }, + "hits": {"hits": [], "max_score": 0.0, "total": 45556}, + "timed_out": False, + "took": 18, } - macros = kumascript.macro_page_count(locale='en-US') + macros = kumascript.macro_page_count(locale="en-US") es_json = { - 'size': 0, - 'query': {'bool': { - 'filter': [{'term': {'locale': 'en-US'}}], - }}, - 'aggs': {'usage': {'terms': { - 'field': 'kumascript_macros', - 'size': 2000} - }}, + "size": 0, + "query": {"bool": {"filter": [{"term": {"locale": "en-US"}}]}}, + "aggs": {"usage": {"terms": {"field": "kumascript_macros", "size": 2000}}}, } mock_es_client.search.assert_called_once_with( - body=es_json, - doc_type=['wiki_document'], - index=['mdn-main_index']) - assert macros == {'a11yrolequicklinks': 100, 'othermacro': 30} + body=es_json, doc_type=["wiki_document"], index=["mdn-main_index"] + ) + assert macros == {"a11yrolequicklinks": 100, "othermacro": 30} -@mock.patch('kuma.wiki.kumascript.macro_page_count') -@mock.patch('kuma.wiki.kumascript.macro_sources') +@mock.patch("kuma.wiki.kumascript.macro_page_count") +@mock.patch("kuma.wiki.kumascript.macro_sources") def test_macro_usage(mock_sources, mock_page_count): mock_sources.return_value = { - 'A11yRoleQuicklinks': 'A11yRoleQuicklinks.ejs', - 'APIFeatureList': 'APIFeatureList.ejs', + "A11yRoleQuicklinks": "A11yRoleQuicklinks.ejs", + "APIFeatureList": "APIFeatureList.ejs", } - all_page_count = {'a11yrolequicklinks': 200, 'othermacro': 50} - en_page_count = {'a11yrolequicklinks': 101, 'othermacro': 42} + all_page_count = {"a11yrolequicklinks": 200, "othermacro": 50} + en_page_count = {"a11yrolequicklinks": 101, "othermacro": 42} mock_page_count.side_effect = [all_page_count, en_page_count] usage = kumascript.macro_usage() expected = { - 'A11yRoleQuicklinks': { - 'github_subpath': 'A11yRoleQuicklinks.ejs', - 'count': 200, - 'en_count': 101, + "A11yRoleQuicklinks": { + "github_subpath": "A11yRoleQuicklinks.ejs", + "count": 200, + "en_count": 101, + }, + "APIFeatureList": { + "github_subpath": "APIFeatureList.ejs", + "count": 0, + "en_count": 0, }, - 'APIFeatureList': { - 'github_subpath': 'APIFeatureList.ejs', - 'count': 0, - 'en_count': 0 - } } assert usage == expected -@mock.patch('kuma.wiki.kumascript.macro_page_count') -@mock.patch('kuma.wiki.kumascript.macro_sources') +@mock.patch("kuma.wiki.kumascript.macro_page_count") +@mock.patch("kuma.wiki.kumascript.macro_sources") def test_macro_usage_empty_kumascript(mock_sources, mock_page_count): """When KumaScript returns an empty response, macro usage is empty.""" mock_sources.return_value = {} - mock_page_count.side_effect = Exception('should not be called') + mock_page_count.side_effect = Exception("should not be called") macros = kumascript.macro_usage() assert macros == {} -@mock.patch('kuma.wiki.kumascript.macro_page_count') -@mock.patch('kuma.wiki.kumascript.macro_sources') +@mock.patch("kuma.wiki.kumascript.macro_page_count") +@mock.patch("kuma.wiki.kumascript.macro_sources") def test_macro_usage_elasticsearch_exception(mock_sources, mock_page_count): """When ElasticSearch is unreachable, counts are 0.""" - mock_sources.return_value = { - 'A11yRoleQuicklinks': 'A11yRoleQuicklinks.ejs' - } + mock_sources.return_value = {"A11yRoleQuicklinks": "A11yRoleQuicklinks.ejs"} mock_page_count.side_effect = TransportError("Can't reach ElasticSearch") macros = kumascript.macro_usage() expected = { - 'A11yRoleQuicklinks': { - 'github_subpath': 'A11yRoleQuicklinks.ejs', - 'count': 0, - 'en_count': 0, + "A11yRoleQuicklinks": { + "github_subpath": "A11yRoleQuicklinks.ejs", + "count": 0, + "en_count": 0, } } assert macros == expected -@mock.patch('kuma.wiki.kumascript.macro_page_count') -@mock.patch('kuma.wiki.kumascript.macro_sources') +@mock.patch("kuma.wiki.kumascript.macro_page_count") +@mock.patch("kuma.wiki.kumascript.macro_sources") def test_macro_usage_2nd_es_exception(mock_sources, mock_page_count): """When follow-on ElasticSearch call raises, reraise exception.""" - mock_sources.return_value = { - 'A11yRoleQuicklinks': 'A11yRoleQuicklinks.ejs' - } + mock_sources.return_value = {"A11yRoleQuicklinks": "A11yRoleQuicklinks.ejs"} mock_page_count.side_effect = [ - {'a11yrolequicklinks': 200, 'othermacro': 50}, - TransportError("Can't reach ElasticSearch") + {"a11yrolequicklinks": 200, "othermacro": 50}, + TransportError("Can't reach ElasticSearch"), ] with pytest.raises(TransportError): kumascript.macro_usage() -@pytest.mark.parametrize('exc_cls', [ConnectionError, ReadTimeout]) +@pytest.mark.parametrize("exc_cls", [ConnectionError, ReadTimeout]) def test_get_with_requests_exception(root_doc, mock_requests, exc_cls): """Test that connection and timeout errors are handled for get.""" - mock_requests.post(requests_mock.ANY, exc=exc_cls('some I/O error')) - body, errors = kumascript.get(root_doc, 'https://example.com', timeout=1) + mock_requests.post(requests_mock.ANY, exc=exc_cls("some I/O error")) + body, errors = kumascript.get(root_doc, "https://example.com", timeout=1) assert body == root_doc.html - assert errors == [{ - 'level': 'error', - 'message': 'some I/O error', - 'args': [exc_cls.__name__] - }] + assert errors == [ + {"level": "error", "message": "some I/O error", "args": [exc_cls.__name__]} + ] -@pytest.mark.parametrize('exc_cls', [ContentDecodingError, TooManyRedirects]) +@pytest.mark.parametrize("exc_cls", [ContentDecodingError, TooManyRedirects]) def test_get_with_other_exception(root_doc, mock_requests, exc_cls): """Test that non-connection/non-timeout errors are not handled for get.""" - mock_requests.post(requests_mock.ANY, exc=exc_cls('requires attention')) + mock_requests.post(requests_mock.ANY, exc=exc_cls("requires attention")) with pytest.raises(exc_cls): - kumascript.get(root_doc, 'https://example.com', timeout=1) + kumascript.get(root_doc, "https://example.com", timeout=1) -@pytest.mark.parametrize('exc_cls', [ConnectionError, ReadTimeout]) +@pytest.mark.parametrize("exc_cls", [ConnectionError, ReadTimeout]) def test_post_with_requests_exception(db, rf, mock_requests, exc_cls): """Test that connection and timeout errors are handled for post.""" - content = 'some freshly edited content' - request = rf.get('/en-US/docs/preview-wiki-content') - mock_requests.post(requests_mock.ANY, exc=exc_cls('some I/O error')) + content = "some freshly edited content" + request = rf.get("/en-US/docs/preview-wiki-content") + mock_requests.post(requests_mock.ANY, exc=exc_cls("some I/O error")) body, errors = kumascript.post(request, content) assert body == content - assert errors == [{ - 'level': 'error', - 'message': 'some I/O error', - 'args': [exc_cls.__name__] - }] + assert errors == [ + {"level": "error", "message": "some I/O error", "args": [exc_cls.__name__]} + ] -@pytest.mark.parametrize('exc_cls', [ContentDecodingError, TooManyRedirects]) +@pytest.mark.parametrize("exc_cls", [ContentDecodingError, TooManyRedirects]) def test_post_with_other_exception(db, rf, mock_requests, exc_cls): """Test that non-connection/non-timeout errors are not handled for post.""" - content = 'some freshly edited content' - request = rf.get('/en-US/docs/preview-wiki-content') - mock_requests.post(requests_mock.ANY, exc=exc_cls('requires attention')) + content = "some freshly edited content" + request = rf.get("/en-US/docs/preview-wiki-content") + mock_requests.post(requests_mock.ANY, exc=exc_cls("requires attention")) with pytest.raises(exc_cls): kumascript.post(request, content) diff --git a/kuma/wiki/tests/test_managers.py b/kuma/wiki/tests/test_managers.py index 05589059508..82cd3783053 100644 --- a/kuma/wiki/tests/test_managers.py +++ b/kuma/wiki/tests/test_managers.py @@ -1,25 +1,25 @@ - - from datetime import datetime import pytest from ..constants import REDIRECT_CONTENT -from ..models import (Document, DocumentTag, LocalizationTag, ReviewTag, - Revision) +from ..models import Document, DocumentTag, LocalizationTag, ReviewTag, Revision @pytest.fixture def redirect_doc(root_doc, wiki_user): """A redirect document.""" - html = REDIRECT_CONTENT % {'href': root_doc.get_absolute_url(), - 'title': root_doc.title} - doc = Document.objects.create(locale='en-US', slug='OldRoot', html=html) + html = REDIRECT_CONTENT % { + "href": root_doc.get_absolute_url(), + "title": root_doc.title, + } + doc = Document.objects.create(locale="en-US", slug="OldRoot", html=html) doc.current_revision = Revision.objects.create( document=doc, creator=wiki_user, content=html, - created=datetime(2017, 10, 27, 17, 4)) + created=datetime(2017, 10, 27, 17, 4), + ) doc.save() return doc @@ -27,26 +27,31 @@ def redirect_doc(root_doc, wiki_user): @pytest.fixture def archive_doc(wiki_user): """An archived document.""" - archive = Document.objects.create(locale='en-US', slug='Archive', - title='Archive of obsolete content') - doc = Document.objects.create(locale='en-US', slug='Archive/Doc', - title='Archived document', - parent_topic=archive) + archive = Document.objects.create( + locale="en-US", slug="Archive", title="Archive of obsolete content" + ) + doc = Document.objects.create( + locale="en-US", + slug="Archive/Doc", + title="Archived document", + parent_topic=archive, + ) doc.current_revision = Revision.objects.create( document=doc, creator=wiki_user, - content='

    Obsolete content.

    ', - comment='Obsolete content', - created=datetime(2017, 10, 27, 15, 48)) + content="

    Obsolete content.

    ", + comment="Obsolete content", + created=datetime(2017, 10, 27, 15, 48), + ) doc.save() return doc def test_get_queryset(root_doc): """Managers are customized for including / excluding deleted documents.""" - deleted_doc = Document.objects.create(locale='en-US', slug='Deleted', - title='Deleted Document', - deleted=True) + deleted_doc = Document.objects.create( + locale="en-US", slug="Deleted", title="Deleted Document", deleted=True + ) assert Document.admin_objects.all().count() == 2 assert list(Document.objects.all()) == [root_doc] assert list(Document.deleted_objects.all()) == [deleted_doc] @@ -55,21 +60,24 @@ def test_get_queryset(root_doc): def test_get_natural_key(root_doc): """The locale + slug is the natural key for Documents.""" assert root_doc.natural_key() == (root_doc.locale, root_doc.slug) - assert root_doc == Document.objects.get_by_natural_key(root_doc.locale, - root_doc.slug) + assert root_doc == Document.objects.get_by_natural_key( + root_doc.locale, root_doc.slug + ) @pytest.mark.parametrize( - 'legacy_slug', - ('User:ethertank', - 'Talk:Developer_Guide/Build_Instructions/Windows_Prerequisites', - 'User_talk:ethertank', - 'Template_talk:anch', - 'Project_talk:To-do_list', - )) + "legacy_slug", + ( + "User:ethertank", + "Talk:Developer_Guide/Build_Instructions/Windows_Prerequisites", + "User_talk:ethertank", + "Template_talk:anch", + "Project_talk:To-do_list", + ), +) def test_documents_filter_for_list_exclude_slug_prefixes(root_doc, legacy_slug): """filter_for_list excludes some slug prefixes.""" - Document.objects.create(locale='en-US', slug=legacy_slug) + Document.objects.create(locale="en-US", slug=legacy_slug) results = Document.objects.filter_for_list() assert len(results) == 1 assert results[0] == root_doc @@ -88,8 +96,8 @@ def test_documents_filter_for_list_by_locale(root_doc, trans_doc): def test_documents_filter_for_list_by_tag(root_doc): """filter_for_list can filter by a DocumentTag.""" - tag_foo = DocumentTag.objects.create(name='foo') - tag_bar = DocumentTag.objects.create(name='bar') + tag_foo = DocumentTag.objects.create(name="foo") + tag_bar = DocumentTag.objects.create(name="bar") root_doc.tags.add(tag_foo) assert list(Document.objects.filter_for_list(tag=tag_foo)) == [root_doc] assert len(Document.objects.filter_for_list(tag=tag_bar)) == 0 @@ -97,17 +105,17 @@ def test_documents_filter_for_list_by_tag(root_doc): def test_documents_filter_for_list_by_tag_name(root_doc): """filter_for_list can filter by a DocumentTag name.""" - tag_foo = DocumentTag.objects.create(name='foo') + tag_foo = DocumentTag.objects.create(name="foo") root_doc.tags.add(tag_foo) - assert list(Document.objects.filter_for_list(tag_name='foo')) == [root_doc] - assert len(Document.objects.filter_for_list(tag_name='bar')) == 0 + assert list(Document.objects.filter_for_list(tag_name="foo")) == [root_doc] + assert len(Document.objects.filter_for_list(tag_name="bar")) == 0 def test_documents_filter_for_list_by_errors(root_doc): """filter_for_list can filter by Documents with render errors.""" assert root_doc.rendered_errors is None assert len(Document.objects.filter_for_list(errors=True)) == 0 - root_doc.rendered_errors = '[]' + root_doc.rendered_errors = "[]" root_doc.save() assert len(Document.objects.filter_for_list(errors=True)) == 0 root_doc.rendered_errors = '[{"name": "kumascript", "level": "error"}]' @@ -118,22 +126,22 @@ def test_documents_filter_for_list_by_errors(root_doc): def test_documents_filter_for_list_by_noparent(root_doc, trans_doc): """filter_for_list can filter by Documents with no parent.""" assert list(Document.objects.filter_for_list(noparent=True)) == [root_doc] - result = Document.objects.filter_for_list(locale=trans_doc.locale, - noparent=True) + result = Document.objects.filter_for_list(locale=trans_doc.locale, noparent=True) assert len(result) == 0 trans_doc.parent = None trans_doc.save() - result = Document.objects.filter_for_list(locale=trans_doc.locale, - noparent=True) + result = Document.objects.filter_for_list(locale=trans_doc.locale, noparent=True) assert list(result) == [trans_doc] def test_documents_filter_for_list_by_toplevel(root_doc): """filter_for_list can filter by top-level Documents (no parent topic).""" - Document.objects.create(locale=root_doc.locale, - parent_topic=root_doc, - slug=root_doc.slug + '/Child', - title='Child Document') + Document.objects.create( + locale=root_doc.locale, + parent_topic=root_doc, + slug=root_doc.slug + "/Child", + title="Child Document", + ) assert len(Document.objects.filter_for_list()) == 2 assert list(Document.objects.filter_for_list(toplevel=True)) == [root_doc] @@ -142,15 +150,15 @@ def test_documents_filter_for_review(create_revision): """filter_for_review can filter all documents with a review tag.""" assert len(Document.objects.filter_for_review()) == 0 - create_revision.review_tags.set('tag') + create_revision.review_tags.set("tag") doc = create_revision.document assert list(Document.objects.filter_for_review()) == [doc] def test_documents_filter_for_review_by_locale(create_revision, trans_revision): """filter_for_review can filter by locale.""" - create_revision.review_tags.set('tag') - trans_revision.review_tags.set('other_tag') + create_revision.review_tags.set("tag") + trans_revision.review_tags.set("other_tag") assert len(Document.objects.filter_for_review()) == 2 doc = create_revision.document @@ -160,19 +168,19 @@ def test_documents_filter_for_review_by_locale(create_revision, trans_revision): def test_documents_filter_for_review_by_tag_name(create_revision): """filter_for_review can filter by ReviewTtag name.""" - assert len(Document.objects.filter_for_review(tag_name='editorial')) == 0 - assert len(Document.objects.filter_for_review(tag_name='technical')) == 0 + assert len(Document.objects.filter_for_review(tag_name="editorial")) == 0 + assert len(Document.objects.filter_for_review(tag_name="technical")) == 0 - create_revision.review_tags.set('editorial') - resp = list(Document.objects.filter_for_review(tag_name='editorial')) + create_revision.review_tags.set("editorial") + resp = list(Document.objects.filter_for_review(tag_name="editorial")) assert resp == [create_revision.document] - assert len(Document.objects.filter_for_review(tag_name='technical')) == 0 + assert len(Document.objects.filter_for_review(tag_name="technical")) == 0 def test_documents_filter_for_review_by_tag(create_revision): """filter_for_review can filter by ReviewTag instance.""" - editorial, _ = ReviewTag.objects.get_or_create(name='editorial') - technical, _ = ReviewTag.objects.get_or_create(name='technical') + editorial, _ = ReviewTag.objects.get_or_create(name="editorial") + technical, _ = ReviewTag.objects.get_or_create(name="technical") assert len(Document.objects.filter_for_review(tag=editorial)) == 0 assert len(Document.objects.filter_for_review(tag=technical)) == 0 @@ -184,15 +192,15 @@ def test_documents_filter_for_review_by_tag(create_revision): def test_documents_filter_for_review_excludes_redirects(redirect_doc): """bug 1274874: filter_for_review excludes redirects.""" - redirect_doc.current_revision.review_tags.set('editorial') - resp = Document.objects.filter_for_review(tag_name='editorial') + redirect_doc.current_revision.review_tags.set("editorial") + resp = Document.objects.filter_for_review(tag_name="editorial") assert len(resp) == 0 def test_documents_filter_for_review_excludes_archive(archive_doc): """bug 1274874: filter_for_review excludes archive documents.""" - archive_doc.current_revision.review_tags.set('editorial') - resp = Document.objects.filter_for_review(tag_name='editorial') + archive_doc.current_revision.review_tags.set("editorial") + resp = Document.objects.filter_for_review(tag_name="editorial") assert len(resp) == 0 @@ -200,16 +208,17 @@ def test_documents_filter_with_localization_tag(create_revision): """filter_with_localization_tag can filter all documents.""" assert len(Document.objects.filter_with_localization_tag()) == 0 - create_revision.localization_tags.set('tag') + create_revision.localization_tags.set("tag") doc = create_revision.document assert list(Document.objects.filter_with_localization_tag()) == [doc] -def test_documents_filter_with_localization_tag_by_locale(create_revision, - trans_revision): +def test_documents_filter_with_localization_tag_by_locale( + create_revision, trans_revision +): """filter_with_localization_tag can filter by locale.""" - create_revision.localization_tags.set('tag') - trans_revision.localization_tags.set('other_tag') + create_revision.localization_tags.set("tag") + trans_revision.localization_tags.set("other_tag") assert len(Document.objects.filter_with_localization_tag()) == 2 doc = create_revision.document @@ -219,18 +228,18 @@ def test_documents_filter_with_localization_tag_by_locale(create_revision, def test_documents_filter_with_localization_tag_by_tag_name(create_revision): """filter_with_localization_tag can filter by LocalizationTag name.""" - tag = 'inprogress' + tag = "inprogress" resp = Document.objects.filter_with_localization_tag(tag_name=tag) assert len(resp) == 0 - create_revision.localization_tags.set('inprogress') + create_revision.localization_tags.set("inprogress") resp = Document.objects.filter_with_localization_tag(tag_name=tag) assert list(resp) == [create_revision.document] def test_documents_filter_with_localization_tag_by_tag(create_revision): """filter_with_localization_tag can filter by the instance.""" - inprogress = LocalizationTag.objects.create(name='inprogress') + inprogress = LocalizationTag.objects.create(name="inprogress") resp = Document.objects.filter_with_localization_tag(tag=inprogress) assert len(resp) == 0 @@ -239,10 +248,9 @@ def test_documents_filter_with_localization_tag_by_tag(create_revision): assert list(resp) == [create_revision.document] -def test_documents_filter_with_localization_tag_excludes_redirects( - redirect_doc): +def test_documents_filter_with_localization_tag_excludes_redirects(redirect_doc): """bug 1274874: filter_with_localization_tag excludes redirects.""" - tag = 'inprogress' + tag = "inprogress" redirect_doc.current_revision.localization_tags.set(tag) resp = Document.objects.filter_with_localization_tag(tag_name=tag) assert len(resp) == 0 @@ -250,7 +258,7 @@ def test_documents_filter_with_localization_tag_excludes_redirects( def test_documents_filter_with_localization_tag_excludes_archive(archive_doc): """bug 1274874: filter_with_localization_tag excludes archive docs.""" - tag = 'inprogress' + tag = "inprogress" archive_doc.current_revision.localization_tags.set(tag) resp = Document.objects.filter_with_localization_tag(tag_name=tag) assert len(resp) == 0 diff --git a/kuma/wiki/tests/test_models.py b/kuma/wiki/tests/test_models.py index fd86a1016dd..8f8c0cb9a49 100644 --- a/kuma/wiki/tests/test_models.py +++ b/kuma/wiki/tests/test_models.py @@ -20,10 +20,12 @@ from .. import tasks from ..constants import EXPERIMENT_TITLE_PREFIX, REDIRECT_CONTENT from ..events import EditDocumentInTreeEvent -from ..exceptions import (DocumentRenderedContentNotAvailable, - DocumentRenderingInProgress, PageMoveError) -from ..models import (Document, DocumentTag, Revision, RevisionIP, - TaggedDocument) +from ..exceptions import ( + DocumentRenderedContentNotAvailable, + DocumentRenderingInProgress, + PageMoveError, +) +from ..models import Document, DocumentTag, Revision, RevisionIP, TaggedDocument from ..utils import tidy_content @@ -36,17 +38,16 @@ def test_clean_current_revision_with_no_current(root_doc, wiki_user_2): assert root_doc.clean_current_revision(wiki_user_2) is None -@pytest.mark.parametrize('is_approved', (True, False)) -@pytest.mark.parametrize('doc_case', ('default-language', 'translation')) -def test_clean_current_revision(root_doc, trans_doc, wiki_user_2, doc_case, - is_approved): - doc = trans_doc if doc_case == 'translation' else root_doc +@pytest.mark.parametrize("is_approved", (True, False)) +@pytest.mark.parametrize("doc_case", ("default-language", "translation")) +def test_clean_current_revision( + root_doc, trans_doc, wiki_user_2, doc_case, is_approved +): + doc = trans_doc if doc_case == "translation" else root_doc original_doc_slug = doc.slug original_doc_title = doc.title current_rev = doc.current_revision - current_rev.content = ( - '
    click me
    ' - ) + current_rev.content = "
    click me
    " current_rev.tidied_content = """ @@ -61,20 +62,20 @@ def test_clean_current_revision(root_doc, trans_doc, wiki_user_2, doc_case, """ tags = '"Banana" "Orange" "Apple"' - l10n_tags = {'inprogress'} - review_tags = {'editorial', 'technical'} + l10n_tags = {"inprogress"} + review_tags = {"editorial", "technical"} current_rev.tags = tags # Let's make the revision's slug and title different from the document # to ensure that they're corrected in the end. - current_rev.slug = original_doc_slug + 's' - current_rev.title = original_doc_title + 's' + current_rev.slug = original_doc_slug + "s" + current_rev.title = original_doc_title + "s" current_rev.is_approved = is_approved current_rev.localization_tags.set(*l10n_tags) current_rev.review_tags.set(*review_tags) prior_pk = current_rev.pk prior_creator = current_rev.creator prior_created = current_rev.created - if doc_case == 'translation': + if doc_case == "translation": expected_based_on_pk = current_rev.based_on.pk else: expected_based_on_pk = current_rev.pk @@ -85,25 +86,26 @@ def test_clean_current_revision(root_doc, trans_doc, wiki_user_2, doc_case, assert rev.creator == wiki_user_2 assert rev.created > prior_created assert rev.based_on.pk == expected_based_on_pk - assert rev.content == '
    click me
    ' + assert rev.content == "
    click me
    " assert rev.tidied_content == ( '\n' - '\n' - ' \n' - ' \n' - ' \n' - ' \n' - '
    \n' - ' click me\n' - '
    \n' - ' \n' - '\n' + "\n" + " \n" + " \n" + " \n" + " \n" + "
    \n" + " click me\n" + "
    \n" + " \n" + "\n" ) assert rev.tags == tags assert set(t.name for t in rev.localization_tags.all()) == l10n_tags assert set(t.name for t in rev.review_tags.all()) == review_tags - assert rev.comment == 'Clean prior revision of {} by {}'.format( - prior_created, prior_creator) + assert rev.comment == "Clean prior revision of {} by {}".format( + prior_created, prior_creator + ) assert rev.slug == original_doc_slug assert rev.title == original_doc_title assert doc.current_revision.pk == rev.pk @@ -111,35 +113,38 @@ def test_clean_current_revision(root_doc, trans_doc, wiki_user_2, doc_case, def test_document_is_not_experiment(): """A document without the experiment prefix is not an experiment.""" - doc = Document(slug='test') + doc = Document(slug="test") assert not doc.is_experiment def test_document_is_experiment(): """A document with the experiment prefix is an experiment.""" - doc = Document(slug=EXPERIMENT_TITLE_PREFIX + 'test') + doc = Document(slug=EXPERIMENT_TITLE_PREFIX + "test") assert doc.is_experiment -@pytest.mark.parametrize('slug,legacy', [ - # See LEGACY_MINDTOUCH_NAMESPACES in ../constants.py - ('Help:Login', True), - ('Help_talk:Login', True), - ('Project:MDN', True), - ('Project_talk:MDN', True), - ('Special:easter_egg', True), - ('Talk:Web:CSS', True), - ('Template:domxref', True), - ('Template_talk:domxref', True), - ('User:jezdez', True), - ('User_talk:jezdez', True), - # Experiments aren't legacy yet - ('Experiment:Blue', False), - # Slugs without colons don't have namespaces - ('CSS', False), - # Slugs with colons might not be legacy - (':hover', False) -]) +@pytest.mark.parametrize( + "slug,legacy", + [ + # See LEGACY_MINDTOUCH_NAMESPACES in ../constants.py + ("Help:Login", True), + ("Help_talk:Login", True), + ("Project:MDN", True), + ("Project_talk:MDN", True), + ("Special:easter_egg", True), + ("Talk:Web:CSS", True), + ("Template:domxref", True), + ("Template_talk:domxref", True), + ("User:jezdez", True), + ("User_talk:jezdez", True), + # Experiments aren't legacy yet + ("Experiment:Blue", False), + # Slugs without colons don't have namespaces + ("CSS", False), + # Slugs with colons might not be legacy + (":hover", False), + ], +) def test_document_has_legacy_namespace(slug, legacy): """Excluded slugs should not update the search index.""" assert Document(slug=slug).has_legacy_namespace == legacy @@ -147,7 +152,7 @@ def test_document_has_legacy_namespace(slug, legacy): def test_document_delete_removes_tag_relationsip(root_doc): """Deleting a tagged document also deletes the tag relationship.""" - root_doc.tags.add('grape') + root_doc.tags.add("grape") assert TaggedDocument.objects.count() == 1 root_doc.delete() assert TaggedDocument.objects.count() == 0 @@ -157,7 +162,7 @@ def test_document_raises_error_when_translating_non_localizable(root_doc): """Adding a translation of a non-localizable document raises an error.""" root_doc.is_localizable = False root_doc.save() - de_doc = Document(parent=root_doc, slug='Rübe', locale='de') + de_doc = Document(parent=root_doc, slug="Rübe", locale="de") with pytest.raises(ValidationError): de_doc.save() @@ -172,15 +177,14 @@ def test_document_raises_error_setting_non_loc_for_trans_doc(trans_doc): def test_document_non_english_implies_non_localizable(db): """All non-English documents are set non-localizable.""" - es_doc = Document.objects.create(locale='es', slug='Tubérculos') + es_doc = Document.objects.create(locale="es", slug="Tubérculos") assert not es_doc.is_localizable def test_document_translations(trans_doc): """other_translations lists other translations, English first.""" en_doc = trans_doc.parent - ar_doc = Document.objects.create(locale='ar', slug='جذور الخضروات', - parent=en_doc) + ar_doc = Document.objects.create(locale="ar", slug="جذور الخضروات", parent=en_doc) # Translations are returned English first, then ordered, and omit self assert ar_doc.locale < en_doc.locale < trans_doc.locale assert en_doc.other_translations == [ar_doc, trans_doc] @@ -191,44 +195,43 @@ def test_document_translations(trans_doc): def test_document_parents(root_doc): """Document.parents gives the document hierarchy.""" assert root_doc.parents == [] - child_doc = Document.objects.create(parent_topic=root_doc, - slug=root_doc.slug + '/Child') + child_doc = Document.objects.create( + parent_topic=root_doc, slug=root_doc.slug + "/Child" + ) assert child_doc.parents == [root_doc] - gchild_doc = Document.objects.create(parent_topic=child_doc, - slug=child_doc.slug + '/GrandChild') + gchild_doc = Document.objects.create( + parent_topic=child_doc, slug=child_doc.slug + "/GrandChild" + ) assert gchild_doc.parents == [root_doc, child_doc] -@pytest.mark.parametrize('url', - (settings.SITE_URL + '/en-US/Mozilla', - '/en-US/Mozilla', - '/', - )) +@pytest.mark.parametrize( + "url", (settings.SITE_URL + "/en-US/Mozilla", "/en-US/Mozilla", "/",) +) def test_document_redirect_allows_valid_url(db, url): """get_redirect_url returns valid URLs.""" - title = 'Mozilla' - html = REDIRECT_CONTENT % {'href': url, 'title': title} - doc = Document.objects.create(locale='en-US', slug='Redirect', - is_redirect=True, html=html) + title = "Mozilla" + html = REDIRECT_CONTENT % {"href": url, "title": title} + doc = Document.objects.create( + locale="en-US", slug="Redirect", is_redirect=True, html=html + ) parsed = urlparse(url) assert doc.get_redirect_url() == parsed.path -@pytest.mark.parametrize('url', - ('//evilsite.com', - 'https://example.com/foriegn_url', - )) +@pytest.mark.parametrize("url", ("//evilsite.com", "https://example.com/foriegn_url",)) def test_document_redirect_rejects_invalid_url(db, url): """get_redirect_url returns None for invalid URLs.""" - html = REDIRECT_CONTENT % {'href': url, 'title': 'Invalid URL'} - doc = Document.objects.create(locale='en-US', slug='Redirect', - is_redirect=True, html=html) + html = REDIRECT_CONTENT % {"href": url, "title": "Invalid URL"} + doc = Document.objects.create( + locale="en-US", slug="Redirect", is_redirect=True, html=html + ) assert doc.get_redirect_url() is None def test_document_get_full_url(root_doc): """get_full_url returns full URLs.""" - assert root_doc.get_full_url() == settings.SITE_URL + '/en-US/docs/Root' + assert root_doc.get_full_url() == settings.SITE_URL + "/en-US/docs/Root" def test_document_from_url(root_doc): @@ -240,7 +243,7 @@ def test_document_from_url(root_doc): def test_document_from_url_locale_matches_translation(trans_doc): """from_url matches translation with locale plus English slug.""" en_doc = trans_doc.parent - url = reverse('wiki.document', locale=trans_doc.locale, args=[en_doc.slug]) + url = reverse("wiki.document", locale=trans_doc.locale, args=[en_doc.slug]) doc = Document.from_url(url) assert doc == trans_doc @@ -248,8 +251,9 @@ def test_document_from_url_locale_matches_translation(trans_doc): def test_document_from_url_bad_slug_returns_none(trans_doc): """from_url returns None for an invalid slug.""" en_doc = trans_doc.parent - url = reverse('wiki.document', locale=trans_doc.locale, - args=[en_doc.slug + '_bad_slug']) + url = reverse( + "wiki.document", locale=trans_doc.locale, args=[en_doc.slug + "_bad_slug"] + ) doc = Document.from_url(url) assert doc is None @@ -268,23 +272,24 @@ def test_document_from_url_full_url_returns_doc(root_doc): def test_document_from_url_other_url_returns_none(root_doc): """from_url returns None for a different domain.""" - assert settings.SITE_URL != 'https://example.com' - url = 'https://example.com' + root_doc.get_absolute_url() + assert settings.SITE_URL != "https://example.com" + url = "https://example.com" + root_doc.get_absolute_url() assert Document.from_url(url) is None def test_document_get_redirect_document(root_doc): """get_redirect_document returns the destination document.""" old_slug = root_doc.slug - root_doc._move_tree(new_slug='Moved') + root_doc._move_tree(new_slug="Moved") old_doc = Document.objects.get(slug=old_slug) assert old_doc.get_redirect_document() == root_doc -@pytest.mark.parametrize('invalidate_cdn_cache', (True, False)) -@mock.patch('kuma.wiki.models.render_done') -def test_document_render_invalidate_cdn_cache(mock_render_done, root_doc, - invalidate_cdn_cache): +@pytest.mark.parametrize("invalidate_cdn_cache", (True, False)) +@mock.patch("kuma.wiki.models.render_done") +def test_document_render_invalidate_cdn_cache( + mock_render_done, root_doc, invalidate_cdn_cache +): """ The "invalidate_cdn_cache" argument to render is passed through as one of the arguments that the "render_done" signal provides. @@ -293,7 +298,7 @@ def test_document_render_invalidate_cdn_cache(mock_render_done, root_doc, mock_render_done.send.assert_called_once_with( sender=root_doc.__class__, instance=root_doc, - invalidate_cdn_cache=invalidate_cdn_cache + invalidate_cdn_cache=invalidate_cdn_cache, ) @@ -303,29 +308,36 @@ class UserDocumentTests(UserTestCase): def test_default_topic_parents_for_translation(self): """A translated document with no topic parent should by default use the translation of its translation parent's topic parent.""" - orig_pt = document(locale=settings.WIKI_DEFAULT_LANGUAGE, - title='test section', - save=True) - orig = document(locale=settings.WIKI_DEFAULT_LANGUAGE, title='test', - parent_topic=orig_pt, save=True) + orig_pt = document( + locale=settings.WIKI_DEFAULT_LANGUAGE, title="test section", save=True + ) + orig = document( + locale=settings.WIKI_DEFAULT_LANGUAGE, + title="test", + parent_topic=orig_pt, + save=True, + ) - trans_pt = document(locale='fr', title='le test section', - parent=orig_pt, save=True) - trans = document(locale='fr', title='le test', - parent=orig, save=True) + trans_pt = document( + locale="fr", title="le test section", parent=orig_pt, save=True + ) + trans = document(locale="fr", title="le test", parent=orig, save=True) assert trans.parent_topic assert trans_pt.pk == trans.parent_topic.pk def test_default_topic_with_stub_creation(self): - orig_pt = document(locale=settings.WIKI_DEFAULT_LANGUAGE, - title='test section', - save=True) - orig = document(locale=settings.WIKI_DEFAULT_LANGUAGE, title='test', - parent_topic=orig_pt, save=True) + orig_pt = document( + locale=settings.WIKI_DEFAULT_LANGUAGE, title="test section", save=True + ) + orig = document( + locale=settings.WIKI_DEFAULT_LANGUAGE, + title="test", + parent_topic=orig_pt, + save=True, + ) - trans = document(locale='fr', title='le test', - parent=orig, save=True) + trans = document(locale="fr", title="le test", parent=orig, save=True) # There should be a translation topic parent trans_pt = trans.parent_topic @@ -344,24 +356,25 @@ def test_default_topic_with_stub_creation(self): def test_default_topic_with_path_gaps(self): # Build a path of docs in en-US - orig_path = ('MDN', 'web', 'CSS', 'properties', 'banana', 'leaf') + orig_path = ("MDN", "web", "CSS", "properties", "banana", "leaf") docs, doc = [], None for title in orig_path: - doc = document(locale=settings.WIKI_DEFAULT_LANGUAGE, title=title, - parent_topic=doc, save=True) + doc = document( + locale=settings.WIKI_DEFAULT_LANGUAGE, + title=title, + parent_topic=doc, + save=True, + ) revision(document=doc, title=title, save=True) docs.append(doc) # Translate, but leave gaps for stubs - trans_0 = document(locale='fr', title='le MDN', - parent=docs[0], save=True) - revision(document=trans_0, title='le MDN', tags="LeTest!", save=True) - trans_2 = document(locale='fr', title='le CSS', - parent=docs[2], save=True) - revision(document=trans_2, title='le CSS', tags="LeTest!", save=True) - trans_5 = document(locale='fr', title='le leaf', - parent=docs[5], save=True) - revision(document=trans_5, title='le ;eaf', tags="LeTest!", save=True) + trans_0 = document(locale="fr", title="le MDN", parent=docs[0], save=True) + revision(document=trans_0, title="le MDN", tags="LeTest!", save=True) + trans_2 = document(locale="fr", title="le CSS", parent=docs[2], save=True) + revision(document=trans_2, title="le CSS", tags="LeTest!", save=True) + trans_5 = document(locale="fr", title="le leaf", parent=docs[5], save=True) + revision(document=trans_5, title="le ;eaf", tags="LeTest!", save=True) # Make sure trans_2 got the right parent assert trans_2.parents[0].pk == trans_0.pk @@ -383,39 +396,41 @@ def test_default_topic_with_path_gaps(self): for p in parents_5: assert p.current_revision if p.pk not in (trans_0.pk, trans_2.pk, trans_5.pk): - assert 'NeedsTranslation' in p.current_revision.tags - assert 'TopicStub' in p.current_revision.tags + assert "NeedsTranslation" in p.current_revision.tags + assert "TopicStub" in p.current_revision.tags assert p.current_revision.localization_in_progress def test_repair_breadcrumbs(self): - english_top = document(locale=settings.WIKI_DEFAULT_LANGUAGE, - title='English top', - save=True) - english_mid = document(locale=settings.WIKI_DEFAULT_LANGUAGE, - title='English mid', - parent_topic=english_top, - save=True) - english_bottom = document(locale=settings.WIKI_DEFAULT_LANGUAGE, - title='English bottom', - parent_topic=english_mid, - save=True) - - french_top = document(locale='fr', - title='French top', - parent=english_top, - save=True) - french_mid = document(locale='fr', - parent=english_mid, - parent_topic=english_mid, - save=True) - french_bottom = document(locale='fr', - parent=english_bottom, - parent_topic=english_bottom, - save=True) + english_top = document( + locale=settings.WIKI_DEFAULT_LANGUAGE, title="English top", save=True + ) + english_mid = document( + locale=settings.WIKI_DEFAULT_LANGUAGE, + title="English mid", + parent_topic=english_top, + save=True, + ) + english_bottom = document( + locale=settings.WIKI_DEFAULT_LANGUAGE, + title="English bottom", + parent_topic=english_mid, + save=True, + ) + + french_top = document( + locale="fr", title="French top", parent=english_top, save=True + ) + french_mid = document( + locale="fr", parent=english_mid, parent_topic=english_mid, save=True + ) + french_bottom = document( + locale="fr", parent=english_bottom, parent_topic=english_bottom, save=True + ) french_bottom.repair_breadcrumbs() - french_bottom_fixed = Document.objects.get(locale='fr', - title=french_bottom.title) + french_bottom_fixed = Document.objects.get( + locale="fr", title=french_bottom.title + ) assert french_mid.id == french_bottom_fixed.parent_topic.id assert french_top.id == french_bottom_fixed.parent_topic.parent_topic.id @@ -423,7 +438,7 @@ def test_code_sample_extraction(self): """Make sure sample extraction works from the model. This is a smaller version of the test from test_content.py""" sample_html = '

    Hello world!

    ' - sample_css = '.foo p { color: red; }' + sample_css = ".foo p { color: red; }" sample_js = 'window.alert("Hi there!");' doc_src = """

    This is a page. Deal with it.

    @@ -433,24 +448,28 @@ def test_code_sample_extraction(self):
  • %s
  • More content shows up here.

    - """ % (escape(sample_html), escape(sample_css), escape(sample_js)) + """ % ( + escape(sample_html), + escape(sample_css), + escape(sample_js), + ) rev = revision(is_approved=True, save=True, content=doc_src) - result = rev.document.extract.code_sample('s2') - assert sample_html.strip() == result['html'].strip() - assert sample_css.strip() == result['css'].strip() - assert sample_js.strip() == result['js'].strip() + result = rev.document.extract.code_sample("s2") + assert sample_html.strip() == result["html"].strip() + assert sample_css.strip() == result["css"].strip() + assert sample_js.strip() == result["js"].strip() def test_tree_is_watched_by(self): rev = revision() - testuser2 = get_user(username='testuser2') + testuser2 = get_user(username="testuser2") EditDocumentInTreeEvent.notify(testuser2, rev.document) assert rev.document.tree_is_watched_by(testuser2) def test_parent_trees_watched_by(self): root_doc, child_doc, grandchild_doc = create_document_tree() - testuser2 = get_user(username='testuser2') + testuser2 = get_user(username="testuser2") EditDocumentInTreeEvent.notify(testuser2, root_doc) EditDocumentInTreeEvent.notify(testuser2, child_doc) @@ -464,38 +483,46 @@ class TaggedDocumentTests(UserTestCase): def test_revision_tags(self): """Change tags on Document by creating Revisions""" - rev = revision(is_approved=True, save=True, content='Sample document') + rev = revision(is_approved=True, save=True, content="Sample document") - assert 0 == Document.objects.filter(tags__name='foo').count() - assert 0 == Document.objects.filter(tags__name='alpha').count() + assert 0 == Document.objects.filter(tags__name="foo").count() + assert 0 == Document.objects.filter(tags__name="alpha").count() - r = revision(document=rev.document, content='Update to document', - is_approved=True, tags="foo, bar, baz") + r = revision( + document=rev.document, + content="Update to document", + is_approved=True, + tags="foo, bar, baz", + ) r.save() - assert 1 == Document.objects.filter(tags__name='foo').count() - assert 0 == Document.objects.filter(tags__name='alpha').count() + assert 1 == Document.objects.filter(tags__name="foo").count() + assert 0 == Document.objects.filter(tags__name="alpha").count() - r = revision(document=rev.document, content='Another update', - is_approved=True, tags="alpha, beta, gamma") + r = revision( + document=rev.document, + content="Another update", + is_approved=True, + tags="alpha, beta, gamma", + ) r.save() - assert 0 == Document.objects.filter(tags__name='foo').count() - assert 1 == Document.objects.filter(tags__name='alpha').count() + assert 0 == Document.objects.filter(tags__name="foo").count() + assert 1 == Document.objects.filter(tags__name="alpha").count() def test_duplicate_tags_with_creation(self): rev = revision( - is_approved=True, save=True, content='Sample document', - tags="test Test") + is_approved=True, save=True, content="Sample document", tags="test Test" + ) assert rev.document.tags.count() == 1 tag = rev.document.tags.get() - assert tag.name in ('test', 'Test') + assert tag.name in ("test", "Test") def test_duplicate_tags_with_existing(self): - dt = DocumentTag.objects.create(name='Test') + dt = DocumentTag.objects.create(name="Test") rev = revision( - is_approved=True, save=True, content='Sample document', - tags="test Test") + is_approved=True, save=True, content="Sample document", tags="test Test" + ) assert rev.document.tags.count() == 1 tag = rev.document.tags.get() assert tag == dt @@ -506,38 +533,43 @@ class RevisionTests(UserTestCase): def test_approved_revision_updates_html(self): """Creating an approved revision updates document.html""" - rev = revision(is_approved=True, save=True, - content='Replace document html') + rev = revision(is_approved=True, save=True, content="Replace document html") - assert 'Replace document html' in rev.document.html, \ - '"Replace document html" not in %s' % rev.document.html + assert "Replace document html" in rev.document.html, ( + '"Replace document html" not in %s' % rev.document.html + ) # Creating another approved revision replaces it again - r = revision(document=rev.document, content='Replace html again', - is_approved=True) + r = revision( + document=rev.document, content="Replace html again", is_approved=True + ) r.save() - assert 'Replace html again' in rev.document.html, \ - '"Replace html again" not in %s' % rev.document.html + assert "Replace html again" in rev.document.html, ( + '"Replace html again" not in %s' % rev.document.html + ) def test_unapproved_revision_not_updates_html(self): """Creating an unapproved revision does not update document.html""" - rev = revision(is_approved=True, save=True, content='Here to stay') + rev = revision(is_approved=True, save=True, content="Here to stay") - assert 'Here to stay' in rev.document.html, \ - '"Here to stay" not in %s' % rev.document.html + assert "Here to stay" in rev.document.html, ( + '"Here to stay" not in %s' % rev.document.html + ) # Creating another approved revision keeps initial content - r = revision(document=rev.document, content='Fail to replace html', - is_approved=False) + r = revision( + document=rev.document, content="Fail to replace html", is_approved=False + ) r.save() - assert 'Here to stay' in rev.document.html, \ - '"Here to stay" not in %s' % rev.document.html + assert "Here to stay" in rev.document.html, ( + '"Here to stay" not in %s' % rev.document.html + ) def test_revision_unicode(self): """Revision containing unicode characters is saved successfully.""" - content = 'Firefox informa\xe7\xf5es \u30d8\u30eb' + content = "Firefox informa\xe7\xf5es \u30d8\u30eb" rev = revision(is_approved=True, save=True, content=content) assert content == rev.content @@ -563,7 +595,7 @@ def test_correct_based_on_to_current_revision(self): en_rev.save() # Make Deutsch translation: - de_doc = document(parent=en_rev.document, locale='de') + de_doc = document(parent=en_rev.document, locale="de") de_doc.save() de_rev = revision(document=de_doc) @@ -575,19 +607,33 @@ def test_correct_based_on_to_current_revision(self): def test_previous(self): """Revision.previous should return this revision's document's most recent approved revision.""" - rev = revision(is_approved=True, created=datetime(2017, 4, 15, 9, 23), - save=True) - next_rev = revision(document=rev.document, content="Updated", - is_approved=True, - created=datetime(2017, 4, 15, 9, 24), save=True) - last_rev = revision(document=rev.document, content="Finally", - is_approved=True, - created=datetime(2017, 4, 15, 9, 25), save=True) - trans = Document.objects.create(parent=rev.document, locale='fr', - title='In French') - trans_rev = revision(document=trans, is_approved=True, - based_on=last_rev, - created=datetime(2017, 4, 15, 9, 56), save=True) + rev = revision( + is_approved=True, created=datetime(2017, 4, 15, 9, 23), save=True + ) + next_rev = revision( + document=rev.document, + content="Updated", + is_approved=True, + created=datetime(2017, 4, 15, 9, 24), + save=True, + ) + last_rev = revision( + document=rev.document, + content="Finally", + is_approved=True, + created=datetime(2017, 4, 15, 9, 25), + save=True, + ) + trans = Document.objects.create( + parent=rev.document, locale="fr", title="In French" + ) + trans_rev = revision( + document=trans, + is_approved=True, + based_on=last_rev, + created=datetime(2017, 4, 15, 9, 56), + save=True, + ) assert rev.previous is None assert next_rev.previous == rev @@ -598,65 +644,72 @@ def test_previous(self): def test_show_toc(self): """Setting toc_depth appropriately affects the Document's show_toc property.""" - rev = revision(is_approved=True, save=True, - content='Toggle table of contents.') - assert (rev.toc_depth != 0) + rev = revision(is_approved=True, save=True, content="Toggle table of contents.") + assert rev.toc_depth != 0 assert rev.document.show_toc - r = revision(document=rev.document, content=rev.content, toc_depth=0, - is_approved=True) + r = revision( + document=rev.document, content=rev.content, toc_depth=0, is_approved=True + ) r.save() assert not rev.document.show_toc - r = revision(document=rev.document, content=r.content, toc_depth=1, - is_approved=True) + r = revision( + document=rev.document, content=r.content, toc_depth=1, is_approved=True + ) r.save() assert rev.document.show_toc def test_revert(self): """Reverting to a specific revision.""" - rev = revision(is_approved=True, save=True, content='Test reverting') + rev = revision(is_approved=True, save=True, content="Test reverting") old_id = rev.id - revision(document=rev.document, - title='Test reverting', - content='An edit to revert', - comment='This edit gets reverted', - is_approved=True) + revision( + document=rev.document, + title="Test reverting", + content="An edit to revert", + comment="This edit gets reverted", + is_approved=True, + ) rev.save() reverted = rev.document.revert(rev, rev.creator) - assert 'Revert to' in reverted.comment - assert 'Test reverting' == reverted.content + assert "Revert to" in reverted.comment + assert "Test reverting" == reverted.content assert old_id != reverted.id def test_revert_review_tags(self): - rev = revision(is_approved=True, save=True, - content='Test reverting with review tags') - rev.review_tags.set('technical') - - r2 = revision(document=rev.document, - title='Test reverting with review tags', - content='An edit to revert', - comment='This edit gets reverted', - is_approved=True) + rev = revision( + is_approved=True, save=True, content="Test reverting with review tags" + ) + rev.review_tags.set("technical") + + r2 = revision( + document=rev.document, + title="Test reverting with review tags", + content="An edit to revert", + comment="This edit gets reverted", + is_approved=True, + ) r2.save() - r2.review_tags.set('editorial') + r2.review_tags.set("editorial") reverted = rev.document.revert(rev, rev.creator) reverted_tags = [t.name for t in reverted.review_tags.all()] - assert 'technical' in reverted_tags - assert 'editorial' not in reverted_tags + assert "technical" in reverted_tags + assert "editorial" not in reverted_tags def test_get_tidied_content_uses_model_field_first(self): - content = '

    Test get_tidied_content.

    ' - fake_tidied = '

    Fake tidied.

    ' - rev = revision(is_approved=True, save=True, content=content, - tidied_content=fake_tidied) + content = "

    Test get_tidied_content.

    " + fake_tidied = "

    Fake tidied.

    " + rev = revision( + is_approved=True, save=True, content=content, tidied_content=fake_tidied + ) assert fake_tidied == rev.get_tidied_content() def test_get_tidied_content_tidies_in_process_by_default(self): - content = '

    Test get_tidied_content

    ' + content = "

    Test get_tidied_content

    " rev = revision(is_approved=True, save=True, content=content) tidied_content, errors = tidy_content( '

    Test get_tidied_content

    ' @@ -664,14 +717,18 @@ def test_get_tidied_content_tidies_in_process_by_default(self): assert tidied_content == rev.get_tidied_content() def test_get_tidied_content_returns_none_on_allow_none(self): - rev = revision(is_approved=True, save=True, - content='Test get_tidied_content can return None.') + rev = revision( + is_approved=True, + save=True, + content="Test get_tidied_content can return None.", + ) assert rev.get_tidied_content(allow_none=True) is None class GetCurrentOrLatestRevisionTests(UserTestCase): """Tests for current_or_latest_revision.""" + def test_single_approved(self): """Get approved revision.""" rev = revision(is_approved=True, save=True) @@ -685,22 +742,22 @@ def test_multiple_approved(self): def test_latest(self): """Return latest revision when no current exists.""" - r1 = revision(is_approved=False, save=True, - created=datetime.now() - timedelta(days=1)) + r1 = revision( + is_approved=False, save=True, created=datetime.now() - timedelta(days=1) + ) r2 = revision(is_approved=False, save=True, document=r1.document) assert r2 == r1.document.current_or_latest_revision() @override_config( - KUMA_DOCUMENT_RENDER_TIMEOUT=600.0, - KUMA_DOCUMENT_FORCE_DEFERRED_TIMEOUT=7.0) + KUMA_DOCUMENT_RENDER_TIMEOUT=600.0, KUMA_DOCUMENT_FORCE_DEFERRED_TIMEOUT=7.0 +) class DeferredRenderingTests(UserTestCase): - def setUp(self): super(DeferredRenderingTests, self).setUp() - self.rendered_content = 'THIS IS RENDERED' - self.raw_content = 'THIS IS NOT RENDERED CONTENT' - self.r1 = revision(is_approved=True, save=True, content='Doc 1') + self.rendered_content = "THIS IS RENDERED" + self.raw_content = "THIS IS NOT RENDERED CONTENT" + self.r1 = revision(is_approved=True, save=True, content="Doc 1") self.d1 = self.r1.document config.KUMA_DOCUMENT_RENDER_TIMEOUT = 600.0 config.KUMA_DOCUMENT_FORCE_DEFERRED_TIMEOUT = 7.0 @@ -718,7 +775,7 @@ def test_rendering_fields(self): assert not self.d1.is_rendering_in_progress @override_config(KUMASCRIPT_TIMEOUT=1.0) - @mock.patch('kuma.wiki.kumascript.get') + @mock.patch("kuma.wiki.kumascript.get") def test_get_rendered(self, mock_kumascript_get): """get_rendered() should return rendered content when available, attempt a render() when it's not""" @@ -729,7 +786,7 @@ def test_get_rendered(self, mock_kumascript_get): assert not self.d1.rendered_html assert not self.d1.render_started_at assert not self.d1.last_rendered_at - result_rendered, _ = self.d1.get_rendered(None, 'http://testserver/') + result_rendered, _ = self.d1.get_rendered(None, "http://testserver/") assert mock_kumascript_get.called assert self.rendered_content == result_rendered assert self.rendered_content == self.d1.rendered_html @@ -742,11 +799,11 @@ def test_get_rendered(self, mock_kumascript_get): assert d1_fresh.render_started_at assert d1_fresh.last_rendered_at mock_kumascript_get.called = False - result_rendered, _ = d1_fresh.get_rendered(None, 'http://testserver/') + result_rendered, _ = d1_fresh.get_rendered(None, "http://testserver/") assert not mock_kumascript_get.called assert self.rendered_content == result_rendered - @mock.patch('kuma.wiki.models.render_done') + @mock.patch("kuma.wiki.models.render_done") def test_build_json_on_render(self, mock_render_done): """ A document's json field is refreshed on render(), but not on save() @@ -760,13 +817,13 @@ def test_build_json_on_render(self, mock_render_done): self.d1.render() assert mock_render_done.send.called - @mock.patch('kuma.wiki.kumascript.get') + @mock.patch("kuma.wiki.kumascript.get") @override_config(KUMASCRIPT_TIMEOUT=1.0) def test_get_summary(self, mock_kumascript_get): """ get_summary() should attempt to use rendered """ - mock_kumascript_get.return_value = ('

    summary!

    ', None) + mock_kumascript_get.return_value = ("

    summary!

    ", None) assert not self.d1.rendered_html result_summary = self.d1.get_summary() assert not mock_kumascript_get.called @@ -776,31 +833,30 @@ def test_get_summary(self, mock_kumascript_get): assert self.d1.rendered_html assert mock_kumascript_get.called result_summary = self.d1.get_summary() - assert 'summary!' == result_summary + assert "summary!" == result_summary - @mock.patch('kuma.wiki.kumascript.get') + @mock.patch("kuma.wiki.kumascript.get") def test_one_render_at_a_time(self, mock_kumascript_get): """Only one in-progress rendering should be allowed for a Document""" mock_kumascript_get.return_value = (self.rendered_content, None) self.d1.render_started_at = datetime.now() self.d1.save() with pytest.raises(DocumentRenderingInProgress): - self.d1.render('', 'http://testserver/') + self.d1.render("", "http://testserver/") - @mock.patch('kuma.wiki.kumascript.get') + @mock.patch("kuma.wiki.kumascript.get") @override_config(KUMA_DOCUMENT_RENDER_TIMEOUT=5.0) def test_render_timeout(self, mock_kumascript_get): """ A rendering that has taken too long is no longer considered in progress """ mock_kumascript_get.return_value = (self.rendered_content, None) - self.d1.render_started_at = (datetime.now() - - timedelta(seconds=5.0 + 1)) + self.d1.render_started_at = datetime.now() - timedelta(seconds=5.0 + 1) self.d1.save() # No DocumentRenderingInProgress raised - self.d1.render('', 'http://testserver/') + self.d1.render("", "http://testserver/") - @mock.patch('kuma.wiki.kumascript.get') + @mock.patch("kuma.wiki.kumascript.get") def test_long_render_sets_deferred(self, mock_kumascript_get): """A rendering that takes more than a desired response time marks the document as in need of deferred rendering in the future.""" @@ -814,25 +870,24 @@ def my_kumascript_get(self, base_url, cache_control, timeout): mock_kumascript_get.side_effect = my_kumascript_get config.KUMA_DOCUMENT_FORCE_DEFERRED_TIMEOUT = 2.0 - self.d1.render('', 'http://testserver/') + self.d1.render("", "http://testserver/") assert not self.d1.defer_rendering config.KUMA_DOCUMENT_FORCE_DEFERRED_TIMEOUT = 0.5 - self.d1.render('', 'http://testserver/') + self.d1.render("", "http://testserver/") assert self.d1.defer_rendering config.KUMASCRIPT_TIMEOUT = 0.0 - @mock.patch('kuma.wiki.kumascript.get') - @mock.patch.object(tasks.render_document, 'delay') - def test_schedule_rendering(self, mock_render_document_delay, - mock_kumascript_get): + @mock.patch("kuma.wiki.kumascript.get") + @mock.patch.object(tasks.render_document, "delay") + def test_schedule_rendering(self, mock_render_document_delay, mock_kumascript_get): mock_kumascript_get.return_value = (self.rendered_content, None) # Scheduling for a non-deferred render should happen on the spot. self.d1.defer_rendering = False self.d1.save() assert not self.d1.render_scheduled_at assert not self.d1.last_rendered_at - self.d1.schedule_rendering(None, 'http://testserver/') + self.d1.schedule_rendering(None, "http://testserver/") assert self.d1.render_scheduled_at assert self.d1.last_rendered_at assert not mock_render_document_delay.called @@ -846,7 +901,7 @@ def test_schedule_rendering(self, mock_render_document_delay, self.d1.save() # Scheduling for a deferred render should result in a queued task. - self.d1.schedule_rendering(None, 'http://testserver/') + self.d1.schedule_rendering(None, "http://testserver/") assert self.d1.render_scheduled_at assert not self.d1.last_rendered_at assert mock_render_document_delay.called @@ -857,41 +912,42 @@ def test_schedule_rendering(self, mock_render_document_delay, assert self.d1.is_rendering_scheduled assert not self.d1.is_rendering_in_progress - @mock.patch('kuma.wiki.kumascript.get') - @mock.patch.object(tasks.render_document, 'delay') - def test_immediate_rendering(self, mock_render_document_delay, - mock_kumascript_get): - '''Rendering is immediate when defer_rendering is False''' + @mock.patch("kuma.wiki.kumascript.get") + @mock.patch.object(tasks.render_document, "delay") + def test_immediate_rendering(self, mock_render_document_delay, mock_kumascript_get): + """Rendering is immediate when defer_rendering is False""" mock_kumascript_get.return_value = (self.rendered_content, None) - mock_render_document_delay.side_effect = Exception('Should not be called') - self.d1.rendered_html = '' + mock_render_document_delay.side_effect = Exception("Should not be called") + self.d1.rendered_html = "" self.d1.defer_rendering = False self.d1.save() - result_rendered, _ = self.d1.get_rendered(None, 'http://testserver/') + result_rendered, _ = self.d1.get_rendered(None, "http://testserver/") assert not mock_render_document_delay.called - @mock.patch('kuma.wiki.kumascript.get') - @mock.patch.object(tasks.render_document, 'delay') - def test_deferred_rendering(self, mock_render_document_delay, - mock_kumascript_get): - '''Rendering is deferred when defer_rendering is True.''' - mock_kumascript_get.side_effect = Exception('Should not be called') - self.d1.rendered_html = '' + @mock.patch("kuma.wiki.kumascript.get") + @mock.patch.object(tasks.render_document, "delay") + def test_deferred_rendering(self, mock_render_document_delay, mock_kumascript_get): + """Rendering is deferred when defer_rendering is True.""" + mock_kumascript_get.side_effect = Exception("Should not be called") + self.d1.rendered_html = "" self.d1.defer_rendering = True self.d1.save() with pytest.raises(DocumentRenderedContentNotAvailable): - self.d1.get_rendered(None, 'http://testserver/') + self.d1.get_rendered(None, "http://testserver/") assert mock_render_document_delay.called - @mock.patch('kuma.wiki.kumascript.get') + @mock.patch("kuma.wiki.kumascript.get") def test_errors_stored_correctly(self, mock_kumascript_get): errors = [ - {'level': 'error', 'message': 'This is a fake error', - 'args': ['FakeError']}, + { + "level": "error", + "message": "This is a fake error", + "args": ["FakeError"], + }, ] mock_kumascript_get.return_value = (self.rendered_content, errors) - r_rendered, r_errors = self.d1.get_rendered(None, 'http://testserver/') + r_rendered, r_errors = self.d1.get_rendered(None, "http://testserver/") assert errors, r_errors @@ -902,33 +958,32 @@ def test_find_stale_documents(self): now = datetime.now() # Fresh - d1 = document(title='Aged 1') + d1 = document(title="Aged 1") d1.render_expires = now + timedelta(seconds=100) d1.save() # Stale, exactly now - d2 = document(title='Aged 2') + d2 = document(title="Aged 2") d2.render_expires = now d2.save() # Stale, a little while ago - d3 = document(title='Aged 3') + d3 = document(title="Aged 3") d3.render_expires = now - timedelta(seconds=100) d3.save() stale_docs = Document.objects.get_by_stale_rendering() - assert (sorted([d2.pk, d3.pk]) == - sorted([x.pk for x in stale_docs])) + assert sorted([d2.pk, d3.pk]) == sorted([x.pk for x in stale_docs]) @override_config(KUMASCRIPT_TIMEOUT=1.0) - @mock.patch('kuma.wiki.kumascript.get') + @mock.patch("kuma.wiki.kumascript.get") def test_update_expires_with_max_age(self, mock_kumascript_get): - mock_kumascript_get.return_value = ('MOCK CONTENT', None) + mock_kumascript_get.return_value = ("MOCK CONTENT", None) max_age = 1000 now = datetime.now() - d1 = document(title='Aged 1') + d1 = document(title="Aged 1") d1.render_max_age = max_age d1.save() d1.render() @@ -939,12 +994,12 @@ def test_update_expires_with_max_age(self, mock_kumascript_get): assert d1.render_expires < later + timedelta(seconds=1) @override_config(KUMASCRIPT_TIMEOUT=1.0) - @mock.patch('kuma.wiki.kumascript.get') + @mock.patch("kuma.wiki.kumascript.get") def test_update_expires_without_max_age(self, mock_kumascript_get): - mock_kumascript_get.return_value = ('MOCK CONTENT', None) + mock_kumascript_get.return_value = ("MOCK CONTENT", None) now = datetime.now() - d1 = document(title='Aged 1') + d1 = document(title="Aged 1") d1.render_expires = now - timedelta(seconds=100) d1.save() d1.render() @@ -952,16 +1007,15 @@ def test_update_expires_without_max_age(self, mock_kumascript_get): assert not d1.render_expires @override_config(KUMASCRIPT_TIMEOUT=1.0) - @mock.patch('kuma.wiki.kumascript.get') - @mock.patch.object(tasks.render_document, 'delay') - def test_render_stale(self, mock_render_document_delay, - mock_kumascript_get): - mock_kumascript_get.return_value = ('MOCK CONTENT', None) + @mock.patch("kuma.wiki.kumascript.get") + @mock.patch.object(tasks.render_document, "delay") + def test_render_stale(self, mock_render_document_delay, mock_kumascript_get): + mock_kumascript_get.return_value = ("MOCK CONTENT", None) now = datetime.now() earlier = now - timedelta(seconds=1000) - d1 = document(title='Aged 3') + d1 = document(title="Aged 3") d1.last_rendered_at = earlier d1.render_expires = now - timedelta(seconds=100) d1.save() @@ -980,11 +1034,11 @@ class PageMoveTests(UserTestCase): def test_children_simple(self): """A basic tree with two direct children and no sub-trees on either.""" - d1 = document(title='Parent', save=True) - d2 = document(title='Child', save=True) + d1 = document(title="Parent", save=True) + d2 = document(title="Child", save=True) d2.parent_topic = d1 d2.save() - d3 = document(title='Another child', save=True) + d3 = document(title="Another child", save=True) d3.parent_topic = d1 d3.save() @@ -992,6 +1046,7 @@ def test_children_simple(self): def test_get_descendants_limited(self): """Tests limiting of descendant levels""" + def _make_doc(title, parent=None): doc = document(title=title, save=True) if parent: @@ -999,11 +1054,11 @@ def _make_doc(title, parent=None): doc.save() return doc - parent = _make_doc('Parent') - child1 = _make_doc('Child 1', parent) - child2 = _make_doc('Child 2', parent) - grandchild = _make_doc('GrandChild 1', child1) - _make_doc('Great GrandChild 1', grandchild) + parent = _make_doc("Parent") + child1 = _make_doc("Child 1", parent) + child2 = _make_doc("Child 2", parent) + grandchild = _make_doc("GrandChild 1", child1) + _make_doc("Great GrandChild 1", grandchild) # Test descendant counts assert 4 == len(parent.get_descendants()) # All @@ -1016,30 +1071,29 @@ def _make_doc(title, parent=None): def test_children_complex(self): """A slightly more complex tree, with multiple children, some of which do/don't have their own children.""" - top = document(title='Parent', save=True) + top = document(title="Parent", save=True) - c1 = document(title='Child 1', save=True) + c1 = document(title="Child 1", save=True) c1.parent_topic = top c1.save() - gc1 = document(title='Child of child 1', save=True) + gc1 = document(title="Child of child 1", save=True) gc1.parent_topic = c1 gc1.save() - c2 = document(title='Child 2', save=True) + c2 = document(title="Child 2", save=True) c2.parent_topic = top c2.save() - gc2 = document(title='Child of child 2', save=True) + gc2 = document(title="Child of child 2", save=True) gc2.parent_topic = c2 gc2.save() - gc3 = document(title='Another child of child 2', save=True) + gc3 = document(title="Another child of child 2", save=True) gc3.parent_topic = c2 gc3.save() - ggc1 = document(title='Child of the second child of child 2', - save=True) + ggc1 = document(title="Child of the second child of child 2", save=True) ggc1.parent_topic = gc3 ggc1.save() @@ -1051,17 +1105,15 @@ def test_circular_dependency(self): """Make sure we can detect potential circular dependencies in parent/child relationships.""" # Test detection at one level removed. - parent = document(title='Parent of circular-dependency document', - save=True) - child = document(title='Document with circular dependency') + parent = document(title="Parent of circular-dependency document", save=True) + child = document(title="Document with circular dependency") child.parent_topic = parent child.save() assert child.is_child_of(parent) # And at two levels removed. - grandparent = document(title='Grandparent of ' - 'circular-dependency document') + grandparent = document(title="Grandparent of " "circular-dependency document") parent.parent_topic = grandparent child.save() @@ -1077,51 +1129,63 @@ def test_move_tree(self): # - child1 # - child2 # - grandchild - top = revision(title='Top-level parent for tree moves', - slug='first-level/parent', - is_approved=True, - save=True) + top = revision( + title="Top-level parent for tree moves", + slug="first-level/parent", + is_approved=True, + save=True, + ) old_top_id = top.id top_doc = top.document - child1 = revision(title='First child of tree-move parent', - slug='first-level/second-level/child1', - is_approved=True, - save=True) + child1 = revision( + title="First child of tree-move parent", + slug="first-level/second-level/child1", + is_approved=True, + save=True, + ) old_child1_id = child1.id child1_doc = child1.document child1_doc.parent_topic = top_doc child1_doc.save() - child2 = revision(title='Second child of tree-move parent', - slug='first-level/second-level/child2', - is_approved=True, - save=True) + child2 = revision( + title="Second child of tree-move parent", + slug="first-level/second-level/child2", + is_approved=True, + save=True, + ) old_child2_id = child2.id child2_doc = child2.document child2_doc.parent_topic = top_doc child2.save() - grandchild = revision(title='Child of second child of tree-move parent', - slug='first-level/second-level/third-level/grandchild', - is_approved=True, - save=True) + grandchild = revision( + title="Child of second child of tree-move parent", + slug="first-level/second-level/third-level/grandchild", + is_approved=True, + save=True, + ) old_grandchild_id = grandchild.id grandchild_doc = grandchild.document grandchild_doc.parent_topic = child2_doc grandchild_doc.save() - revision(title='New Top-level bucket for tree moves', - slug='new-prefix', - is_approved=True, - save=True) - revision(title='New first-level parent for tree moves', - slug='new-prefix/first-level', - is_approved=True, - save=True) + revision( + title="New Top-level bucket for tree moves", + slug="new-prefix", + is_approved=True, + save=True, + ) + revision( + title="New first-level parent for tree moves", + slug="new-prefix/first-level", + is_approved=True, + save=True, + ) # Now we do a simple move: inserting a prefix that needs to be # inherited by the whole tree. - top_doc._move_tree('new-prefix/first-level/parent') + top_doc._move_tree("new-prefix/first-level/parent") # And for each document verify three things: # @@ -1129,230 +1193,291 @@ def test_move_tree(self): # 2. A new revision was created when the page moved. # 3. A redirect was created. moved_top = Document.objects.get(pk=top_doc.id) - assert ('new-prefix/first-level/parent' == - moved_top.current_revision.slug) + assert "new-prefix/first-level/parent" == moved_top.current_revision.slug assert old_top_id != moved_top.current_revision.id - assert (moved_top.current_revision.slug in - Document.objects.get(slug='first-level/parent').get_redirect_url()) + assert ( + moved_top.current_revision.slug + in Document.objects.get(slug="first-level/parent").get_redirect_url() + ) moved_child1 = Document.objects.get(pk=child1_doc.id) - assert ('new-prefix/first-level/parent/child1' == - moved_child1.current_revision.slug) + assert ( + "new-prefix/first-level/parent/child1" == moved_child1.current_revision.slug + ) assert old_child1_id != moved_child1.current_revision.id - assert moved_child1.current_revision.slug in Document.objects.get( - slug='first-level/second-level/child1').get_redirect_url() + assert ( + moved_child1.current_revision.slug + in Document.objects.get( + slug="first-level/second-level/child1" + ).get_redirect_url() + ) moved_child2 = Document.objects.get(pk=child2_doc.id) - assert ('new-prefix/first-level/parent/child2' == - moved_child2.current_revision.slug) + assert ( + "new-prefix/first-level/parent/child2" == moved_child2.current_revision.slug + ) assert old_child2_id != moved_child2.current_revision.id - assert moved_child2.current_revision.slug in Document.objects.get( - slug='first-level/second-level/child2').get_redirect_url() + assert ( + moved_child2.current_revision.slug + in Document.objects.get( + slug="first-level/second-level/child2" + ).get_redirect_url() + ) moved_grandchild = Document.objects.get(pk=grandchild_doc.id) - assert('new-prefix/first-level/parent/child2/grandchild' == - moved_grandchild.current_revision.slug) + assert ( + "new-prefix/first-level/parent/child2/grandchild" + == moved_grandchild.current_revision.slug + ) assert old_grandchild_id != moved_grandchild.current_revision.id - assert moved_grandchild.current_revision.slug in Document.objects.get( - slug='first-level/second-level/third-level/grandchild').get_redirect_url() + assert ( + moved_grandchild.current_revision.slug + in Document.objects.get( + slug="first-level/second-level/third-level/grandchild" + ).get_redirect_url() + ) @pytest.mark.move def test_conflicts(self): - top = revision(title='Test page-move conflict detection', - slug='test-move-conflict-detection', - is_approved=True, - save=True) + top = revision( + title="Test page-move conflict detection", + slug="test-move-conflict-detection", + is_approved=True, + save=True, + ) top_doc = top.document - child = revision(title='Child of conflict detection test', - slug='move-tests/conflict-child', - is_approved=True, - save=True) + child = revision( + title="Child of conflict detection test", + slug="move-tests/conflict-child", + is_approved=True, + save=True, + ) child_doc = child.document child_doc.parent_topic = top_doc child_doc.save() # We should find the conflict if it's at the slug the document # will move to. - top_conflict = revision(title='Conflicting document for move conflict detection', - slug='moved/test-move-conflict-detection', - is_approved=True, - save=True) + top_conflict = revision( + title="Conflicting document for move conflict detection", + slug="moved/test-move-conflict-detection", + is_approved=True, + save=True, + ) - assert([top_conflict.document] == - top_doc._tree_conflicts('moved/test-move-conflict-detection')) + assert [top_conflict.document] == top_doc._tree_conflicts( + "moved/test-move-conflict-detection" + ) # Or if it will involve a child document. - child_conflict = revision(title='Conflicting child for move conflict detection', - slug='moved/test-move-conflict-detection/conflict-child', - is_approved=True, - save=True) + child_conflict = revision( + title="Conflicting child for move conflict detection", + slug="moved/test-move-conflict-detection/conflict-child", + is_approved=True, + save=True, + ) - assert ([top_conflict.document, child_conflict.document] == - top_doc._tree_conflicts('moved/test-move-conflict-detection')) + assert [ + top_conflict.document, + child_conflict.document, + ] == top_doc._tree_conflicts("moved/test-move-conflict-detection") # But a redirect should not trigger a conflict. - revision(title='Conflicting document for move conflict detection', - slug='moved/test-move-conflict-detection', - content='REDIRECT Foo', - document=top_conflict.document, - is_approved=True, - save=True) + revision( + title="Conflicting document for move conflict detection", + slug="moved/test-move-conflict-detection", + content='REDIRECT Foo', + document=top_conflict.document, + is_approved=True, + save=True, + ) - assert ([child_conflict.document] == - top_doc._tree_conflicts('moved/test-move-conflict-detection')) + assert [child_conflict.document] == top_doc._tree_conflicts( + "moved/test-move-conflict-detection" + ) @pytest.mark.move def test_additional_conflicts(self): - top = revision(title='WebRTC', - slug='WebRTC', - content='WebRTC', - is_approved=True, - save=True) + top = revision( + title="WebRTC", slug="WebRTC", content="WebRTC", is_approved=True, save=True + ) top_doc = top.document - child1 = revision(title='WebRTC Introduction', - slug='WebRTC/WebRTC_Introduction', - content='WebRTC Introduction', - is_approved=True, - save=True) + child1 = revision( + title="WebRTC Introduction", + slug="WebRTC/WebRTC_Introduction", + content="WebRTC Introduction", + is_approved=True, + save=True, + ) child1_doc = child1.document child1_doc.parent_topic = top_doc child1_doc.save() - child2 = revision(title='Taking webcam photos', - slug='WebRTC/Taking_webcam_photos', - is_approved=True, - save=True) + child2 = revision( + title="Taking webcam photos", + slug="WebRTC/Taking_webcam_photos", + is_approved=True, + save=True, + ) child2_doc = child2.document child2_doc.parent_topic = top_doc child2_doc.save() - assert not top_doc._tree_conflicts('NativeRTC') + assert not top_doc._tree_conflicts("NativeRTC") @pytest.mark.move def test_preserve_tags(self): tags = "'moving', 'tests'" - rev = revision(title='Test page-move tag preservation', - slug='page-move-tags', - tags=tags, - is_approved=True, - save=True) - rev.review_tags.set('technical') + rev = revision( + title="Test page-move tag preservation", + slug="page-move-tags", + tags=tags, + is_approved=True, + save=True, + ) + rev.review_tags.set("technical") rev = Revision.objects.get(pk=rev.id) - revision(title='New Top-level parent for tree moves', - slug='new-top', - is_approved=True, - save=True) + revision( + title="New Top-level parent for tree moves", + slug="new-top", + is_approved=True, + save=True, + ) doc = rev.document - doc._move_tree('new-top/page-move-tags') + doc._move_tree("new-top/page-move-tags") moved_doc = Document.objects.get(pk=doc.id) new_rev = moved_doc.current_revision assert tags == new_rev.tags - assert (['technical'] == - [str(tag) for tag in new_rev.review_tags.all()]) + assert ["technical"] == [str(tag) for tag in new_rev.review_tags.all()] @pytest.mark.move def test_move_tree_breadcrumbs(self): """Moving a tree of documents under an existing doc updates breadcrumbs""" - grandpa = revision(title='Top-level parent for breadcrumb move', - slug='grandpa', is_approved=True, save=True) + grandpa = revision( + title="Top-level parent for breadcrumb move", + slug="grandpa", + is_approved=True, + save=True, + ) grandpa_doc = grandpa.document - dad = revision(title='Mid-level parent for breadcrumb move', - slug='grandpa/dad', is_approved=True, save=True) + dad = revision( + title="Mid-level parent for breadcrumb move", + slug="grandpa/dad", + is_approved=True, + save=True, + ) dad_doc = dad.document dad_doc.parent_topic = grandpa_doc dad_doc.save() - son = revision(title='Bottom-level child for breadcrumb move', - slug='grandpa/dad/son', is_approved=True, save=True) + son = revision( + title="Bottom-level child for breadcrumb move", + slug="grandpa/dad/son", + is_approved=True, + save=True, + ) son_doc = son.document son_doc.parent_topic = dad_doc son_doc.save() - grandma = revision(title='Top-level parent for breadcrumb move', - slug='grandma', is_approved=True, save=True) + grandma = revision( + title="Top-level parent for breadcrumb move", + slug="grandma", + is_approved=True, + save=True, + ) grandma_doc = grandma.document - mom = revision(title='Mid-level parent for breadcrumb move', - slug='grandma/mom', is_approved=True, save=True) + mom = revision( + title="Mid-level parent for breadcrumb move", + slug="grandma/mom", + is_approved=True, + save=True, + ) mom_doc = mom.document mom_doc.parent_topic = grandma_doc mom_doc.save() - daughter = revision(title='Bottom-level child for breadcrumb move', - slug='grandma/mom/daughter', - is_approved=True, - save=True) + daughter = revision( + title="Bottom-level child for breadcrumb move", + slug="grandma/mom/daughter", + is_approved=True, + save=True, + ) daughter_doc = daughter.document daughter_doc.parent_topic = mom_doc daughter_doc.save() # move grandma under grandpa - grandma_doc._move_tree('grandpa/grandma') + grandma_doc._move_tree("grandpa/grandma") # assert the parent_topics are correctly rooted at grandpa # note we have to refetch these to see any DB changes. - grandma_moved = Document.objects.get(locale=grandma_doc.locale, - slug='grandpa/grandma') + grandma_moved = Document.objects.get( + locale=grandma_doc.locale, slug="grandpa/grandma" + ) assert grandma_moved.parent_topic == grandpa_doc - mom_moved = Document.objects.get(locale=mom_doc.locale, - slug='grandpa/grandma/mom') + mom_moved = Document.objects.get( + locale=mom_doc.locale, slug="grandpa/grandma/mom" + ) assert mom_moved.parent_topic == grandma_moved @pytest.mark.move def test_move_tree_no_new_parent(self): """Moving a tree to a slug that doesn't exist throws error.""" - rev = revision(title='doc to move', - slug='doc1', is_approved=True, save=True) + rev = revision(title="doc to move", slug="doc1", is_approved=True, save=True) doc = rev.document with pytest.raises(Exception): - doc._move_tree('slug-that-doesnt-exist/doc1') + doc._move_tree("slug-that-doesnt-exist/doc1") @pytest.mark.move def test_move_top_level_docs(self): """Moving a top document to a new slug location""" - page_to_move_title = 'Page Move Root' - page_to_move_slug = 'Page_Move_Root' - page_child_slug = 'Page_Move_Root/Page_Move_Child' - page_moved_slug = 'Page_Move_Root_Moved' - page_child_moved_slug = 'Page_Move_Root_Moved/Page_Move_Child' - - page_to_move_doc = document(title=page_to_move_title, - slug=page_to_move_slug, - save=True) - rev = revision(document=page_to_move_doc, - title=page_to_move_title, - slug=page_to_move_slug, - save=True) + page_to_move_title = "Page Move Root" + page_to_move_slug = "Page_Move_Root" + page_child_slug = "Page_Move_Root/Page_Move_Child" + page_moved_slug = "Page_Move_Root_Moved" + page_child_moved_slug = "Page_Move_Root_Moved/Page_Move_Child" + + page_to_move_doc = document( + title=page_to_move_title, slug=page_to_move_slug, save=True + ) + rev = revision( + document=page_to_move_doc, + title=page_to_move_title, + slug=page_to_move_slug, + save=True, + ) page_to_move_doc.current_revision = rev page_to_move_doc.save() - page_child = revision(title='child', slug=page_child_slug, - is_approved=True, save=True) + page_child = revision( + title="child", slug=page_child_slug, is_approved=True, save=True + ) page_child_doc = page_child.document page_child_doc.parent_topic = page_to_move_doc page_child_doc.save() # move page to new slug - new_title = page_to_move_title + ' Moved' + new_title = page_to_move_title + " Moved" - page_to_move_doc._move_tree(page_moved_slug, user=None, - title=new_title) + page_to_move_doc._move_tree(page_moved_slug, user=None, title=new_title) page_to_move_doc = Document.objects.get(slug=page_to_move_slug) page_moved_doc = Document.objects.get(slug=page_moved_slug) page_child_doc = Document.objects.get(slug=page_child_slug) page_child_moved_doc = Document.objects.get(slug=page_child_moved_slug) - assert 'REDIRECT' in page_to_move_doc.html + assert "REDIRECT" in page_to_move_doc.html assert page_moved_slug in page_to_move_doc.html assert new_title in page_to_move_doc.html assert page_moved_doc - assert 'REDIRECT' in page_child_doc.html + assert "REDIRECT" in page_child_doc.html assert page_moved_slug in page_child_doc.html assert page_child_moved_doc # TODO: Fix this assertion? @@ -1360,34 +1485,30 @@ def test_move_top_level_docs(self): @pytest.mark.move def test_mid_move(self): - root_title = 'Root' - root_slug = 'Root' - child_title = 'Child' - child_slug = 'Root/Child' - moved_child_slug = 'DiffChild' - grandchild_title = 'Grandchild' - grandchild_slug = 'Root/Child/Grandchild' - moved_grandchild_slug = 'DiffChild/Grandchild' - - root_doc = document(title=root_title, - slug=root_slug, - save=True) - rev = revision(document=root_doc, - title=root_title, - slug=root_slug, - save=True) + root_title = "Root" + root_slug = "Root" + child_title = "Child" + child_slug = "Root/Child" + moved_child_slug = "DiffChild" + grandchild_title = "Grandchild" + grandchild_slug = "Root/Child/Grandchild" + moved_grandchild_slug = "DiffChild/Grandchild" + + root_doc = document(title=root_title, slug=root_slug, save=True) + rev = revision(document=root_doc, title=root_title, slug=root_slug, save=True) root_doc.current_revision = rev root_doc.save() - child = revision(title=child_title, slug=child_slug, - is_approved=True, save=True) + child = revision( + title=child_title, slug=child_slug, is_approved=True, save=True + ) child_doc = child.document child_doc.parent_topic = root_doc child_doc.save() - grandchild = revision(title=grandchild_title, - slug=grandchild_slug, - is_approved=True, save=True) + grandchild = revision( + title=grandchild_title, slug=grandchild_slug, is_approved=True, save=True + ) grandchild_doc = grandchild.document grandchild_doc.parent_topic = child_doc grandchild_doc.save() @@ -1396,36 +1517,33 @@ def test_mid_move(self): redirected_child = Document.objects.get(slug=child_slug) Document.objects.get(slug=moved_child_slug) - assert 'REDIRECT' in redirected_child.html + assert "REDIRECT" in redirected_child.html assert moved_child_slug in redirected_child.html redirected_grandchild = Document.objects.get(slug=grandchild_doc.slug) Document.objects.get(slug=moved_grandchild_slug) - assert 'REDIRECT' in redirected_grandchild.html + assert "REDIRECT" in redirected_grandchild.html assert moved_grandchild_slug in redirected_grandchild.html @pytest.mark.move def test_move_special(self): - root_slug = 'User:foo' - child_slug = '%s/child' % root_slug - - new_root_slug = 'User:foobar' - - special_root = document(title='User:foo', - slug=root_slug, - save=True) - revision(document=special_root, - title=special_root.title, - slug=root_slug, - save=True) - - special_child = document(title='User:foo child', - slug=child_slug, - save=True) - revision(document=special_child, - title=special_child.title, - slug=child_slug, - save=True) + root_slug = "User:foo" + child_slug = "%s/child" % root_slug + + new_root_slug = "User:foobar" + + special_root = document(title="User:foo", slug=root_slug, save=True) + revision( + document=special_root, title=special_root.title, slug=root_slug, save=True + ) + + special_child = document(title="User:foo child", slug=child_slug, save=True) + revision( + document=special_child, + title=special_child.title, + slug=child_slug, + save=True, + ) special_child.parent_topic = special_root special_child.save() @@ -1437,49 +1555,53 @@ def test_move_special(self): special_root._move_tree(new_root_slug) # Appropriate redirects were left behind. - root_redirect = Document.objects.get(locale=special_root.locale, - slug=root_slug) + root_redirect = Document.objects.get(locale=special_root.locale, slug=root_slug) assert root_redirect.is_redirect root_redirect_id = root_redirect.id - child_redirect = Document.objects.get(locale=special_child.locale, - slug=child_slug) + child_redirect = Document.objects.get( + locale=special_child.locale, slug=child_slug + ) assert child_redirect.is_redirect child_redirect_id = child_redirect.id # Moved documents still have the same IDs. - moved_root = Document.objects.get(locale=special_root.locale, - slug=new_root_slug) + moved_root = Document.objects.get( + locale=special_root.locale, slug=new_root_slug + ) assert original_root_id == moved_root.id - moved_child = Document.objects.get(locale=special_child.locale, - slug='%s/child' % new_root_slug) + moved_child = Document.objects.get( + locale=special_child.locale, slug="%s/child" % new_root_slug + ) assert original_child_id == moved_child.id # Second move, back to original slug. moved_root._move_tree(root_slug) # Once again we left redirects behind. - root_second_redirect = Document.objects.get(locale=special_root.locale, - slug=new_root_slug) + root_second_redirect = Document.objects.get( + locale=special_root.locale, slug=new_root_slug + ) assert root_second_redirect.is_redirect - child_second_redirect = Document.objects.get(locale=special_child.locale, - slug='%s/child' % new_root_slug) + child_second_redirect = Document.objects.get( + locale=special_child.locale, slug="%s/child" % new_root_slug + ) assert child_second_redirect.is_redirect # The documents at the original URLs aren't redirects anymore. - rerooted_root = Document.objects.get(locale=special_root.locale, - slug=root_slug) + rerooted_root = Document.objects.get(locale=special_root.locale, slug=root_slug) assert not rerooted_root.is_redirect - rerooted_child = Document.objects.get(locale=special_child.locale, - slug=child_slug) + rerooted_child = Document.objects.get( + locale=special_child.locale, slug=child_slug + ) assert not rerooted_child.is_redirect # The redirects created in the first move no longer exist in the DB. - self.assertRaises(Document.DoesNotExist, - Document.objects.get, - id=root_redirect_id) - self.assertRaises(Document.DoesNotExist, - Document.objects.get, - id=child_redirect_id) + self.assertRaises( + Document.DoesNotExist, Document.objects.get, id=root_redirect_id + ) + self.assertRaises( + Document.DoesNotExist, Document.objects.get, id=child_redirect_id + ) def test_fail_message(self): """ @@ -1488,45 +1610,54 @@ def test_fail_message(self): child document failed. """ - top = revision(title='Test page-move error messaging', - slug='test-move-error-messaging', - is_approved=True, - save=True) + top = revision( + title="Test page-move error messaging", + slug="test-move-error-messaging", + is_approved=True, + save=True, + ) top_doc = top.document - child = revision(title='Child to test page-move error messaging', - slug='test-move-error-messaging/child', - is_approved=True, - save=True) + child = revision( + title="Child to test page-move error messaging", + slug="test-move-error-messaging/child", + is_approved=True, + save=True, + ) child_doc = child.document child_doc.parent_topic = top_doc child_doc.save() - grandchild = revision(title='Grandchild to test page-move error handling', - slug='test-move-error-messaging/child/grandchild', - is_approved=True, - save=True) + grandchild = revision( + title="Grandchild to test page-move error handling", + slug="test-move-error-messaging/child/grandchild", + is_approved=True, + save=True, + ) grandchild_doc = grandchild.document grandchild_doc.parent_topic = child_doc grandchild_doc.save() - revision(title='Conflict page for page-move error handling', - slug='test-move-error-messaging/moved/grandchild', - is_approved=True, - save=True) + revision( + title="Conflict page for page-move error handling", + slug="test-move-error-messaging/moved/grandchild", + is_approved=True, + save=True, + ) mentioned_url = ( - f'https://developer.mozilla.org/{grandchild_doc.locale}' - f'/docs/{grandchild_doc.slug}') + f"https://developer.mozilla.org/{grandchild_doc.locale}" + f"/docs/{grandchild_doc.slug}" + ) with self.assertRaisesRegex(PageMoveError, mentioned_url): - child_doc._move_tree('test-move-error-messaging/moved') + child_doc._move_tree("test-move-error-messaging/moved") class RevisionIPTests(UserTestCase): def test_delete_older_than_default_30_days(self): old_date = date.today() - timedelta(days=31) r = revision(created=old_date, save=True) - RevisionIP.objects.create(revision=r, ip='127.0.0.1').save() + RevisionIP.objects.create(revision=r, ip="127.0.0.1").save() assert 1 == RevisionIP.objects.all().count() RevisionIP.objects.delete_old() assert 0 == RevisionIP.objects.all().count() @@ -1534,7 +1665,7 @@ def test_delete_older_than_default_30_days(self): def test_delete_older_than_days_argument(self): rev_date = date.today() - timedelta(days=5) r = revision(created=rev_date, save=True) - RevisionIP.objects.create(revision=r, ip='127.0.0.1').save() + RevisionIP.objects.create(revision=r, ip="127.0.0.1").save() assert 1 == RevisionIP.objects.all().count() RevisionIP.objects.delete_old(days=4) assert 0 == RevisionIP.objects.all().count() @@ -1542,43 +1673,43 @@ def test_delete_older_than_days_argument(self): def test_delete_older_than_only_deletes_older_than(self): oldest_date = date.today() - timedelta(days=31) r1 = revision(created=oldest_date, save=True) - RevisionIP.objects.create(revision=r1, ip='127.0.0.1').save() + RevisionIP.objects.create(revision=r1, ip="127.0.0.1").save() old_date = date.today() - timedelta(days=29) r1 = revision(created=old_date, save=True) - RevisionIP.objects.create(revision=r1, ip='127.0.0.1').save() + RevisionIP.objects.create(revision=r1, ip="127.0.0.1").save() now_date = date.today() r2 = revision(created=now_date, save=True) - RevisionIP.objects.create(revision=r2, ip='127.0.0.1').save() + RevisionIP.objects.create(revision=r2, ip="127.0.0.1").save() assert 3 == RevisionIP.objects.all().count() RevisionIP.objects.delete_old() assert 2 == RevisionIP.objects.all().count() class AttachmentTests(UserTestCase): - def new_attachment(self, mindtouch_attachment_id=666): attachment = Attachment( - title='test attachment', - mindtouch_attachment_id=mindtouch_attachment_id, + title="test attachment", mindtouch_attachment_id=mindtouch_attachment_id, ) attachment.save() attachment_revision = AttachmentRevision( attachment=attachment, - file='some/path.ext', - mime_type='application/kuma', - creator=get_user(username='admin'), - title='test attachment', + file="some/path.ext", + mime_type="application/kuma", + creator=get_user(username="admin"), + title="test attachment", ) attachment_revision.save() return attachment, attachment_revision def test_popuplate_deki_file_url(self): attachment, attachment_revision = self.new_attachment() - html = ("""%s%s/@api/deki/files/%s/=""" % - (settings.PROTOCOL, settings.ATTACHMENT_HOST, - attachment.mindtouch_attachment_id)) + html = """%s%s/@api/deki/files/%s/=""" % ( + settings.PROTOCOL, + settings.ATTACHMENT_HOST, + attachment.mindtouch_attachment_id, + ) doc = document(html=html, save=True) doc.populate_attachments() @@ -1600,8 +1731,7 @@ def test_popuplate_kuma_file_url(self): def test_popuplate_multiple_attachments(self): attachment, attachment_revision = self.new_attachment() attachment2, attachment_revision2 = self.new_attachment() - html = ("%s %s" % - (attachment.get_file_url(), attachment2.get_file_url())) + html = "%s %s" % (attachment.get_file_url(), attachment2.get_file_url()) doc = document(html=html, save=True) populated = doc.populate_attachments() attachments = doc.attached_files.all() diff --git a/kuma/wiki/tests/test_models_document.py b/kuma/wiki/tests/test_models_document.py index 94cf911cf68..2bb45e15a3b 100644 --- a/kuma/wiki/tests/test_models_document.py +++ b/kuma/wiki/tests/test_models_document.py @@ -35,44 +35,51 @@ def doc_with_sections(root_doc, wiki_user):

    Short

    This is the short section.

    - """ % {'root_url': root_doc.get_absolute_url()} + """ % { + "root_url": root_doc.get_absolute_url() + } root_doc.current_revision = Revision.objects.create( - document=root_doc, content=src, creator=wiki_user) + document=root_doc, content=src, creator=wiki_user + ) root_doc.save() return root_doc -@pytest.mark.parametrize('locales,expected_results', - HREFLANG_TEST_CASES.values(), - ids=tuple(HREFLANG_TEST_CASES.keys())) +@pytest.mark.parametrize( + "locales,expected_results", + HREFLANG_TEST_CASES.values(), + ids=tuple(HREFLANG_TEST_CASES.keys()), +) def test_document_get_hreflang(root_doc, locales, expected_results): docs = [ Document.objects.create( locale=locale, - slug='Root', - title='Root Document', - rendered_html='

    ...

    ', - parent=root_doc - ) for locale in locales + slug="Root", + title="Root Document", + rendered_html="

    ...

    ", + parent=root_doc, + ) + for locale in locales ] for doc, expected_result in zip(docs, expected_results): assert doc.get_hreflang() == expected_result -@pytest.mark.parametrize('locales,expected_results', - HREFLANG_TEST_CASES.values(), - ids=tuple(HREFLANG_TEST_CASES.keys())) -def test_document_get_hreflang_with_other_locales(root_doc, locales, - expected_results): +@pytest.mark.parametrize( + "locales,expected_results", + HREFLANG_TEST_CASES.values(), + ids=tuple(HREFLANG_TEST_CASES.keys()), +) +def test_document_get_hreflang_with_other_locales(root_doc, locales, expected_results): for locale, expected_result in zip(locales, expected_results): doc = Document.objects.create( locale=locale, - slug='Root', - title='Root Document', - rendered_html='

    ...

    ', - parent=root_doc + slug="Root", + title="Root Document", + rendered_html="

    ...

    ", + parent=root_doc, ) - method = 'get_other_translations' + method = "get_other_translations" exc = Exception('"{!r}.{}" unexpectedly called'.format(doc, method)) with mock.patch.object(doc, method, side_effect=exc): assert doc.get_hreflang(locales) == expected_result @@ -94,30 +101,30 @@ def test_get_json_data_cached_parsed_json(root_doc): def test_get_json_data_cached_db_json(root_doc): """Document.get_json_data uses cached JSON stored in DB.""" root_doc.json = '{"stale": "ready for fondue"}' - expected = {'stale': 'ready for fondue'} + expected = {"stale": "ready for fondue"} assert root_doc.get_json_data() == expected -@mock.patch.object(Document, 'build_json_data') +@mock.patch.object(Document, "build_json_data") def test_get_json_data_ignores_bad_cached_db_json(mocked_build, root_doc): """Document.get_json_data ignores bad cached JSON stored in DB.""" - root_doc.json = 'I am invalid' - fresh = {'fresh': 'baked this morning'} + root_doc.json = "I am invalid" + fresh = {"fresh": "baked this morning"} mocked_build.return_value = fresh assert root_doc.get_json_data() == fresh -@mock.patch.object(Document, 'build_json_data') +@mock.patch.object(Document, "build_json_data") def test_get_json_data_detects_stale_cached_db_json(mocked_build, root_doc): """Document.get_json_data will rebuild stale cached JSON if requested.""" old_mod = root_doc.modified - timedelta(seconds=1) root_doc.json = '{"json_modified": "%s"}' % old_mod.isoformat() - fresh = {'fresh': 'still hot'} + fresh = {"fresh": "still hot"} mocked_build.return_value = fresh assert root_doc.get_json_data(stale=False) == fresh -@mock.patch.object(Document, 'build_json_data') +@mock.patch.object(Document, "build_json_data") def test_get_json_data_keeps_cached_db_json(mocked_build, root_doc): """Document.get_json_data does not rebuild fresh cached JSON.""" newer_mod = root_doc.modified + timedelta(seconds=1) @@ -127,11 +134,11 @@ def test_get_json_data_keeps_cached_db_json(mocked_build, root_doc): assert root_doc.get_json_data(stale=False) == newer_json -@mock.patch.object(Document, 'build_json_data') +@mock.patch.object(Document, "build_json_data") def test_get_json_data_maintenance(mocked_build, root_doc, settings): """The JSON data is not cached in read-only maintenance mode.""" settings.MAINTENANCE_MODE = True - mm_json = {'mode': 'MM'} + mm_json = {"mode": "MM"} mocked_build.return_value = mm_json assert root_doc.get_json_data() == mm_json root_doc.refresh_from_db() @@ -147,29 +154,27 @@ def test_build_json_data_unsaved_doc(): code appears to exercise this branch. """ doc = Document( - slug='NewDoc', - title='New Doc', - uuid='765203ea-c5b8-4385-a551-26c1ef9fc843' + slug="NewDoc", title="New Doc", uuid="765203ea-c5b8-4385-a551-26c1ef9fc843" ) new_json = doc.build_json_data() - now_iso = new_json['json_modified'] + now_iso = new_json["json_modified"] expected = { - 'id': None, - 'json_modified': now_iso, - 'label': 'New Doc', - 'last_edit': '', - 'locale': 'en-US', - 'localization_tags': [], - 'modified': now_iso, - 'review_tags': [], - 'sections': [], - 'slug': 'NewDoc', - 'summary': '', - 'tags': [], - 'title': 'New Doc', - 'translations': [], - 'url': '/en-US/docs/NewDoc', - 'uuid': '765203ea-c5b8-4385-a551-26c1ef9fc843' + "id": None, + "json_modified": now_iso, + "label": "New Doc", + "last_edit": "", + "locale": "en-US", + "localization_tags": [], + "modified": now_iso, + "review_tags": [], + "sections": [], + "slug": "NewDoc", + "summary": "", + "tags": [], + "title": "New Doc", + "translations": [], + "url": "/en-US/docs/NewDoc", + "uuid": "765203ea-c5b8-4385-a551-26c1ef9fc843", } assert new_json == expected @@ -179,67 +184,67 @@ def test_build_json_data_with_translations(trans_doc): en_doc = trans_doc.parent en_json = en_doc.build_json_data() en_expected = { - 'id': en_doc.id, - 'json_modified': en_json['json_modified'], - 'label': 'Root Document', - 'last_edit': '2017-04-14T12:15:00', - 'locale': 'en-US', - 'localization_tags': [], - 'modified': en_doc.modified.isoformat(), - 'review_tags': [], - 'sections': [], - 'slug': 'Root', - 'summary': 'Getting started...', - 'tags': [], - 'title': 'Root Document', - 'translations': [ + "id": en_doc.id, + "json_modified": en_json["json_modified"], + "label": "Root Document", + "last_edit": "2017-04-14T12:15:00", + "locale": "en-US", + "localization_tags": [], + "modified": en_doc.modified.isoformat(), + "review_tags": [], + "sections": [], + "slug": "Root", + "summary": "Getting started...", + "tags": [], + "title": "Root Document", + "translations": [ { - 'last_edit': '2017-04-14T12:20:00', - 'locale': 'fr', - 'localization_tags': [], - 'review_tags': [], - 'summary': 'Mise en route...', - 'tags': [], - 'title': 'Racine du Document', - 'url': '/fr/docs/Racine', - 'uuid': str(trans_doc.uuid), + "last_edit": "2017-04-14T12:20:00", + "locale": "fr", + "localization_tags": [], + "review_tags": [], + "summary": "Mise en route...", + "tags": [], + "title": "Racine du Document", + "url": "/fr/docs/Racine", + "uuid": str(trans_doc.uuid), } ], - 'url': '/en-US/docs/Root', - 'uuid': str(en_doc.uuid), + "url": "/en-US/docs/Root", + "uuid": str(en_doc.uuid), } assert en_json == en_expected fr_json = trans_doc.build_json_data() fr_expected = { - 'id': trans_doc.id, - 'json_modified': fr_json['json_modified'], - 'label': 'Racine du Document', - 'last_edit': '2017-04-14T12:20:00', - 'locale': 'fr', - 'localization_tags': [], - 'modified': trans_doc.modified.isoformat(), - 'review_tags': [], - 'sections': [], - 'slug': 'Racine', - 'summary': 'Mise en route...', - 'tags': [], - 'title': 'Racine du Document', - 'translations': [ + "id": trans_doc.id, + "json_modified": fr_json["json_modified"], + "label": "Racine du Document", + "last_edit": "2017-04-14T12:20:00", + "locale": "fr", + "localization_tags": [], + "modified": trans_doc.modified.isoformat(), + "review_tags": [], + "sections": [], + "slug": "Racine", + "summary": "Mise en route...", + "tags": [], + "title": "Racine du Document", + "translations": [ { - 'last_edit': '2017-04-14T12:15:00', - 'locale': 'en-US', - 'localization_tags': [], - 'review_tags': [], - 'summary': 'Getting started...', - 'tags': [], - 'title': 'Root Document', - 'url': '/en-US/docs/Root', - 'uuid': str(en_doc.uuid), + "last_edit": "2017-04-14T12:15:00", + "locale": "en-US", + "localization_tags": [], + "review_tags": [], + "summary": "Getting started...", + "tags": [], + "title": "Root Document", + "url": "/en-US/docs/Root", + "uuid": str(en_doc.uuid), } ], - 'url': '/fr/docs/Racine', - 'uuid': str(trans_doc.uuid), + "url": "/fr/docs/Racine", + "uuid": str(trans_doc.uuid), } assert fr_json == fr_expected @@ -248,29 +253,29 @@ def test_build_json_data_with_tags(trans_doc): """Document JSON includes lists of tags.""" en_doc = trans_doc.parent en_doc.tags.add("NeedsUpdate", "Beginner") - en_doc.current_revision.localization_tags.add('english_already') - en_doc.current_revision.review_tags.add('technical') + en_doc.current_revision.localization_tags.add("english_already") + en_doc.current_revision.review_tags.add("technical") trans_doc.tags.add("NeedsUpdate", "Débutant") - trans_doc.current_revision.localization_tags.add('inprogress') - trans_doc.current_revision.review_tags.add('editorial') + trans_doc.current_revision.localization_tags.add("inprogress") + trans_doc.current_revision.review_tags.add("editorial") en_json = en_doc.build_json_data() - assert sorted(en_json['tags']) == ['Beginner', 'NeedsUpdate'] - assert en_json['localization_tags'] == ['english_already'] - assert en_json['review_tags'] == ['technical'] - fr_json = en_json['translations'][0] - assert sorted(fr_json['tags']) == ["Débutant", "NeedsUpdate"] - assert fr_json['localization_tags'] == ['inprogress'] + assert sorted(en_json["tags"]) == ["Beginner", "NeedsUpdate"] + assert en_json["localization_tags"] == ["english_already"] + assert en_json["review_tags"] == ["technical"] + fr_json = en_json["translations"][0] + assert sorted(fr_json["tags"]) == ["Débutant", "NeedsUpdate"] + assert fr_json["localization_tags"] == ["inprogress"] def test_build_json_data_with_summary(trans_doc): """The summary is the cached HTML SEO description.""" en_doc = trans_doc.parent - en_doc.summary_html = 'Cached Summary' + en_doc.summary_html = "Cached Summary" en_doc.save() en_json = en_doc.build_json_data() - assert en_json['summary'] == 'Cached Summary' - assert en_json['translations'][0]['summary'] == 'Mise en route...' + assert en_json["summary"] == "Cached Summary" + assert en_json["translations"][0]["summary"] == "Mise en route..." def test_build_json_data_uses_rendered_html(root_doc): @@ -290,27 +295,28 @@ def test_build_json_data_uses_rendered_html(root_doc): root_doc.save() json_data = root_doc.build_json_data() expected_sections = [ - {'id': 'Section_1', 'title': 'Section 1'}, - {'id': 'Section_3', 'title': 'Section 3'} + {"id": "Section_1", "title": "Section 1"}, + {"id": "Section_3", "title": "Section 3"}, ] - assert json_data['sections'] == expected_sections + assert json_data["sections"] == expected_sections root_doc.rendered_html = root_doc.html.replace( "{{ h2_macro('Section 2') }}", - "

    Section 2

    \n

    I'm generated by KumaScript

    ") + "

    Section 2

    \n

    I'm generated by KumaScript

    ", + ) root_doc.save() json_data = root_doc.build_json_data() expected_sections = [ - {'id': 'Section_1', 'title': 'Section 1'}, - {'id': 'Section_2', 'title': 'Section 2'}, - {'id': 'Section_3', 'title': 'Section 3'} + {"id": "Section_1", "title": "Section 1"}, + {"id": "Section_2", "title": "Section 2"}, + {"id": "Section_3", "title": "Section 3"}, ] - assert json_data['sections'] == expected_sections + assert json_data["sections"] == expected_sections def test_get_section_content(doc_with_sections): """A section can be extracted by ID.""" - result = doc_with_sections.get_section_content('Short') + result = doc_with_sections.get_section_content("Short") expected = "

    This is the short section.

    " assert normalize_html(result) == normalize_html(expected) diff --git a/kuma/wiki/tests/test_search.py b/kuma/wiki/tests/test_search.py index 77fa3d0a2f4..040e043864c 100644 --- a/kuma/wiki/tests/test_search.py +++ b/kuma/wiki/tests/test_search.py @@ -15,11 +15,10 @@ @pytest.fixture def mock_doc(): """A mock Document that should update.""" - mock_doc = mock.Mock( - spec_set=['is_redirect', 'deleted', 'slug']) + mock_doc = mock.Mock(spec_set=["is_redirect", "deleted", "slug"]) mock_doc.is_redirect = False mock_doc.deleted = False - mock_doc.slug = 'RegularSlug' + mock_doc.slug = "RegularSlug" return mock_doc @@ -29,17 +28,23 @@ def test_should_update_standard_doc(mock_doc): @pytest.mark.parametrize( - 'slug', ('Talk:Web' 'Web/Talk:CSS', 'User:jezdez', - 'User_talk:jezdez', 'Template_talk:anch', - 'Project_talk:MDN', 'Experiment:Blue')) + "slug", + ( + "Talk:Web" "Web/Talk:CSS", + "User:jezdez", + "User_talk:jezdez", + "Template_talk:anch", + "Project_talk:MDN", + "Experiment:Blue", + ), +) def test_should_not_update_excluded_slug(mock_doc, slug): """Excluded slugs should not update the search index.""" mock_doc.slug = slug assert not WikiDocumentType.should_update(mock_doc) -@pytest.mark.parametrize( - 'flag', ('is_redirect', 'deleted')) +@pytest.mark.parametrize("flag", ("is_redirect", "deleted")) def test_should_not_update_excluded_flags(mock_doc, flag): """Do not update the search index if some flags are set.""" setattr(mock_doc, flag, True) diff --git a/kuma/wiki/tests/test_signal_handlers.py b/kuma/wiki/tests/test_signal_handlers.py index 8caaacf52e8..5d4059b6c6d 100644 --- a/kuma/wiki/tests/test_signal_handlers.py +++ b/kuma/wiki/tests/test_signal_handlers.py @@ -1,5 +1,3 @@ - - from unittest import mock import pytest @@ -11,33 +9,31 @@ @pytest.mark.tags def test_on_document_save_signal_invalidated_tags_cache(root_doc, wiki_user): - tags1 = ('JavaScript', 'AJAX', 'DOM') - Revision.objects.create(document=root_doc, tags=','.join(tags1), creator=wiki_user) + tags1 = ("JavaScript", "AJAX", "DOM") + Revision.objects.create(document=root_doc, tags=",".join(tags1), creator=wiki_user) # cache the tags of the document and check its the tag that we created and it is sorted assert sorted(tags1) == root_doc.all_tags_name # Create another revision with some other tags and check tags get invalidate and get updated - tags2 = ('foo', 'bar') - Revision.objects.create(document=root_doc, tags=','.join(tags2), creator=wiki_user) + tags2 = ("foo", "bar") + Revision.objects.create(document=root_doc, tags=",".join(tags2), creator=wiki_user) doc = Document.objects.get(id=root_doc.id) assert sorted(tags2) == doc.all_tags_name -@mock.patch('kuma.wiki.signal_handlers.build_json_data_for_document') +@mock.patch("kuma.wiki.signal_handlers.build_json_data_for_document") def test_render_signal(build_json_task, root_doc): """The JSON is rebuilt when a Document is done rendering.""" - render_done.send( - sender=Document, instance=root_doc, invalidate_cdn_cache=False) + render_done.send(sender=Document, instance=root_doc, invalidate_cdn_cache=False) assert build_json_task.delay.called -@mock.patch('kuma.wiki.signal_handlers.build_json_data_for_document') +@mock.patch("kuma.wiki.signal_handlers.build_json_data_for_document") def test_render_signal_doc_deleted(build_json_task, root_doc): """The JSON is not rebuilt when a deleted Document is done rendering.""" root_doc.deleted = True - render_done.send( - sender=Document, instance=root_doc, invalidate_cdn_cache=False) + render_done.send(sender=Document, instance=root_doc, invalidate_cdn_cache=False) assert not build_json_task.delay.called diff --git a/kuma/wiki/tests/test_ssr.py b/kuma/wiki/tests/test_ssr.py index 7c90070e603..975eec3b4ba 100644 --- a/kuma/wiki/tests/test_ssr.py +++ b/kuma/wiki/tests/test_ssr.py @@ -18,11 +18,12 @@ def sorted_json_dumps(o): real_json_dumps = json.dumps -@pytest.mark.parametrize('locale', ['en-US', 'es']) -@mock.patch('json.dumps') -@mock.patch('kuma.wiki.templatetags.ssr.get_localization_data') -def test_server_side_render(mock_get_l10n_data, mock_dumps, locale, - mock_requests, settings): +@pytest.mark.parametrize("locale", ["en-US", "es"]) +@mock.patch("json.dumps") +@mock.patch("kuma.wiki.templatetags.ssr.get_localization_data") +def test_server_side_render( + mock_get_l10n_data, mock_dumps, locale, mock_requests, settings +): """For server-side rendering expect a div with some content and a script with less data than we'd get for client-side rendering """ @@ -30,89 +31,93 @@ def test_server_side_render(mock_get_l10n_data, mock_dumps, locale, mock_dumps.side_effect = sorted_json_dumps # This is the input to the mock Node server - body = 'article content' - toc = 'table of contents' - links = 'sidebar' - contributors = ['a', 'b'] + body = "article content" + toc = "table of contents" + links = "sidebar" + contributors = ["a", "b"] document_data = { - 'bodyHTML': body, - 'tocHTML': toc, - 'quickLinksHTML': links, - 'contributors': contributors + "bodyHTML": body, + "tocHTML": toc, + "quickLinksHTML": links, + "contributors": contributors, } - localization_data = {'catalog': {'s': locale}, 'plural': None} + localization_data = {"catalog": {"s": locale}, "plural": None} mock_get_l10n_data.side_effect = lambda l: localization_data # This will be the output sent by the mock Node server - mock_html = '

    {}

    {}

    {}

    {}

    '.format( - body, toc, links, contributors) + mock_html = "

    {}

    {}

    {}

    {}

    ".format( + body, toc, links, contributors + ) - url = '{}/{}'.format(settings.SSR_URL, 'document') + url = "{}/{}".format(settings.SSR_URL, "document") - mock_requests.post(url, json={ - 'html': mock_html, - 'script': 'STUFF'}) + mock_requests.post(url, json={"html": mock_html, "script": "STUFF"}) # Run the template tag - path = '/en-US/docs/foo' - output = ssr.render_react('document', locale, path, document_data) + path = "/en-US/docs/foo" + output = ssr.render_react("document", locale, path, document_data) # Make sure the output is as expected data = { - 'locale': locale, - 'url': path, - 'stringCatalog': localization_data['catalog'], - 'documentData': document_data, + "locale": locale, + "url": path, + "stringCatalog": localization_data["catalog"], + "documentData": document_data, } expect = ( '
    {}
    \n' - '\n' - ).format('document', mock_html, json.dumps(data)) + "\n" + ).format("document", mock_html, json.dumps(data)) assert output == expect -@mock.patch('json.dumps') -@mock.patch('kuma.wiki.templatetags.ssr.get_localization_data') +@mock.patch("json.dumps") +@mock.patch("kuma.wiki.templatetags.ssr.get_localization_data") def test_client_side_render(mock_get_l10n_data, mock_dumps): """For client-side rendering expect a script json data and an empty div.""" - localization_data = {'catalog': {'s': 't'}, 'plural': None} + localization_data = {"catalog": {"s": "t"}, "plural": None} mock_get_l10n_data.side_effect = lambda l: localization_data mock_dumps.side_effect = sorted_json_dumps - document_data = {'x': 'one', 'y': 2, 'z': ['a', 'b']} - path = '/en-US/docs/foo' + document_data = {"x": "one", "y": 2, "z": ["a", "b"]} + path = "/en-US/docs/foo" data = { - 'locale': 'en-US', - 'url': path, - 'stringCatalog': localization_data['catalog'], - 'documentData': document_data, - 'pluralExpression': None, + "locale": "en-US", + "url": path, + "stringCatalog": localization_data["catalog"], + "documentData": document_data, + "pluralExpression": None, } - output = ssr.render_react('page', 'en-US', path, document_data, ssr=False) + output = ssr.render_react("page", "en-US", path, document_data, ssr=False) expected = ( '
    \n' - '\n' - ).format('page', json.dumps(data)) + "\n" + ).format("page", json.dumps(data)) assert output == expected -@pytest.mark.parametrize('failure_class', [ - requests.exceptions.ConnectionError, - requests.exceptions.ReadTimeout, - requests.exceptions.HTTPError]) -@mock.patch('json.dumps') -@mock.patch('kuma.wiki.templatetags.ssr.get_localization_data') -def test_failed_server_side_render(mock_get_l10n_data, - mock_dumps, failure_class, - mock_requests, settings): +@pytest.mark.parametrize( + "failure_class", + [ + requests.exceptions.ConnectionError, + requests.exceptions.ReadTimeout, + requests.exceptions.HTTPError, + ], +) +@mock.patch("json.dumps") +@mock.patch("kuma.wiki.templatetags.ssr.get_localization_data") +def test_failed_server_side_render( + mock_get_l10n_data, mock_dumps, failure_class, mock_requests, settings +): """If SSR fails, we should do client-side rendering instead.""" - localization_data = {'catalog': {'s': 't'}, 'plural': None} + localization_data = {"catalog": {"s": "t"}, "plural": None} mock_get_l10n_data.side_effect = lambda l: localization_data mock_dumps.side_effect = sorted_json_dumps - url = f'{settings.SSR_URL}/page' - mock_requests.post(url, exc=failure_class('message')) - document_data = {'x': 'one', 'y': 2, 'z': ['a', 'b']} - path = '/en-US/docs/foo' - assert (ssr.render_react('page', 'en-US', path, document_data) == - ssr.render_react('page', 'en-US', path, document_data, ssr=False)) + url = f"{settings.SSR_URL}/page" + mock_requests.post(url, exc=failure_class("message")) + document_data = {"x": "one", "y": 2, "z": ["a", "b"]} + path = "/en-US/docs/foo" + assert ssr.render_react("page", "en-US", path, document_data) == ssr.render_react( + "page", "en-US", path, document_data, ssr=False + ) diff --git a/kuma/wiki/tests/test_tasks.py b/kuma/wiki/tests/test_tasks.py index 68506ce185b..795b7a787c0 100644 --- a/kuma/wiki/tests/test_tasks.py +++ b/kuma/wiki/tests/test_tasks.py @@ -7,36 +7,30 @@ from kuma.core.urlresolvers import reverse from kuma.users.models import User from kuma.users.tests import UserTestCase -from kuma.wiki.constants import ( - EXPERIMENT_TITLE_PREFIX, - LEGACY_MINDTOUCH_NAMESPACES -) +from kuma.wiki.constants import EXPERIMENT_TITLE_PREFIX, LEGACY_MINDTOUCH_NAMESPACES from kuma.wiki.templatetags.jinja_helpers import absolutify -from ..models import ( - Document, - DocumentDeletionLog, - DocumentSpamAttempt, - Revision +from ..models import Document, DocumentDeletionLog, DocumentSpamAttempt, Revision +from ..tasks import ( + build_sitemaps, + delete_logs_for_purged_documents, + delete_old_documentspamattempt_data, ) -from ..tasks import (build_sitemaps, - delete_logs_for_purged_documents, - delete_old_documentspamattempt_data) def test_sitemaps(tmpdir, settings, doc_hierarchy): """ Test the build of the sitemaps. """ - settings.SITE_URL = 'https://example.com' - settings.MEDIA_ROOT = str(tmpdir.mkdir('media')) + settings.SITE_URL = "https://example.com" + settings.MEDIA_ROOT = str(tmpdir.mkdir("media")) build_sitemaps() - loc_re = re.compile(r'(.+)') - lastmod_re = re.compile(r'(.+)') + loc_re = re.compile(r"(.+)") + lastmod_re = re.compile(r"(.+)") - sitemap_file_path = os.path.join(settings.MEDIA_ROOT, 'sitemap.xml') + sitemap_file_path = os.path.join(settings.MEDIA_ROOT, "sitemap.xml") assert os.path.exists(sitemap_file_path) with open(sitemap_file_path) as file: actual_index_locs = loc_re.findall(file.read()) @@ -46,13 +40,13 @@ def test_sitemaps(tmpdir, settings, doc_hierarchy): expected_index_locs = set() for locale, _ in settings.LANGUAGES: - names = ['sitemap_other.xml'] + names = ["sitemap_other.xml"] actual_locs, actual_lastmods = [], [] docs = Document.objects.filter(locale=locale) if docs.exists(): - names.append('sitemap.xml') + names.append("sitemap.xml") for name in names: - sitemap_path = os.path.join('sitemaps', locale, name) + sitemap_path = os.path.join("sitemaps", locale, name) expected_index_locs.add(absolutify(sitemap_path)) sitemap_file_path = os.path.join(settings.MEDIA_ROOT, sitemap_path) assert os.path.exists(sitemap_file_path) @@ -65,10 +59,10 @@ def test_sitemaps(tmpdir, settings, doc_hierarchy): assert len(actual_locs) == len(set(actual_locs)) expected_locs, expected_lastmods = set(), set() - expected_locs.add(absolutify(reverse('home', locale=locale))) + expected_locs.add(absolutify(reverse("home", locale=locale))) for doc in docs: expected_locs.add(absolutify(doc.get_absolute_url())) - expected_lastmods.add(doc.modified.strftime('%Y-%m-%d')) + expected_lastmods.add(doc.modified.strftime("%Y-%m-%d")) assert set(actual_locs) == expected_locs assert set(actual_lastmods) == expected_lastmods @@ -80,103 +74,92 @@ def test_sitemaps_excluded_documents(tmpdir, settings, wiki_user): """ Test the build of the sitemaps. """ - settings.SITE_URL = 'https://example.com' - settings.MEDIA_ROOT = str(tmpdir.mkdir('media')) + settings.SITE_URL = "https://example.com" + settings.MEDIA_ROOT = str(tmpdir.mkdir("media")) # Simplify the test settings.LANGUAGES = [ (code, english) for code, english in settings.LANGUAGES - if code in ('en-US', 'sv-SE') + if code in ("en-US", "sv-SE") ] - top_doc = Document.objects.create( - locale='en-US', - slug='top', - title='Top Document' - ) + top_doc = Document.objects.create(locale="en-US", slug="top", title="Top Document") Revision.objects.create( document=top_doc, creator=wiki_user, - content='

    Top...

    ', - title='Top Document', - created=datetime(2017, 4, 24, 13, 49) + content="

    Top...

    ", + title="Top Document", + created=datetime(2017, 4, 24, 13, 49), ) # Make one document for every mindtouch legacy namespace. for namespace in LEGACY_MINDTOUCH_NAMESPACES: - legacy_slug = '{}:something'.format(namespace) + legacy_slug = "{}:something".format(namespace) legacy_doc = Document.objects.create( - locale='en-US', - slug=legacy_slug, - title='A Legacy Document' + locale="en-US", slug=legacy_slug, title="A Legacy Document" ) Revision.objects.create( document=legacy_doc, creator=wiki_user, - content='

    Legacy...

    ', - title='Legacy Document', - created=datetime(2017, 4, 24, 13, 49) + content="

    Legacy...

    ", + title="Legacy Document", + created=datetime(2017, 4, 24, 13, 49), ) # Add an "experiment" document - experiment_slug = EXPERIMENT_TITLE_PREFIX + 'myexperiment' + experiment_slug = EXPERIMENT_TITLE_PREFIX + "myexperiment" experiment_doc = Document.objects.create( - locale='en-US', - slug=experiment_slug, - title='An Experiment Document' + locale="en-US", slug=experiment_slug, title="An Experiment Document" ) Revision.objects.create( document=experiment_doc, creator=wiki_user, - content='

    Experiment...

    ', - title='Experiment Document', - created=datetime(2017, 4, 24, 13, 49) + content="

    Experiment...

    ", + title="Experiment Document", + created=datetime(2017, 4, 24, 13, 49), ) # Add a document with no HTML content - no_html_slug = 'a-fine-slug' + no_html_slug = "a-fine-slug" no_html_doc = Document.objects.create( - locale='en-US', - slug=no_html_slug, - title='A Lonely Title' + locale="en-US", slug=no_html_slug, title="A Lonely Title" ) Revision.objects.create( document=no_html_doc, creator=wiki_user, - content='', # Note! - title='Just A Title', - created=datetime(2017, 4, 24, 13, 49) + content="", # Note! + title="Just A Title", + created=datetime(2017, 4, 24, 13, 49), ) assert not no_html_doc.html # Add a document without a revision - no_revision_slug = 'no-revision-slug' + no_revision_slug = "no-revision-slug" experiment_doc = Document.objects.create( - locale='en-US', - slug=no_revision_slug, - title='Has no revision yet' + locale="en-US", slug=no_revision_slug, title="Has no revision yet" ) build_sitemaps() - sitemaps = glob(os.path.join(settings.MEDIA_ROOT, '**/*.xml'), - recursive=True) + sitemaps = glob(os.path.join(settings.MEDIA_ROOT, "**/*.xml"), recursive=True) all_locs = [] for sitemap in sitemaps: with open(sitemap) as f: content = f.read() - matches = re.findall('(.*?)', content) + matches = re.findall("(.*?)", content) all_locs.extend(matches) # Exclude the inter-linking sitemaps - all_locs = [loc for loc in all_locs if not loc.endswith('.xml')] + all_locs = [loc for loc in all_locs if not loc.endswith(".xml")] # Now check exactly which slugs we expect in entirety. # Note that this automatically asserts that all the legacy docs # created above don't get returned. assert {urlparse(loc).path for loc in all_locs} == { - '/en-US/', '/en-US/docs/top', '/sv-SE/' + "/en-US/", + "/en-US/docs/top", + "/sv-SE/", } @@ -184,16 +167,24 @@ class DeleteOldDocumentSpamAttemptData(UserTestCase): fixtures = UserTestCase.fixtures def test_delete_old_data(self): - user = User.objects.get(username='testuser01') - admin = User.objects.get(username='admin') + user = User.objects.get(username="testuser01") + admin = User.objects.get(username="admin") new_dsa = DocumentSpamAttempt.objects.create( - user=user, title='new record', slug='users:me', - data='{"PII": "IP, email, etc."}') + user=user, + title="new record", + slug="users:me", + data='{"PII": "IP, email, etc."}', + ) old_reviewed_dsa = DocumentSpamAttempt.objects.create( - user=user, title='old ham', data='{"PII": "plenty"}', - review=DocumentSpamAttempt.HAM, reviewer=admin) + user=user, + title="old ham", + data='{"PII": "plenty"}', + review=DocumentSpamAttempt.HAM, + reviewer=admin, + ) old_unreviewed_dsa = DocumentSpamAttempt.objects.create( - user=user, title='old unknown', data='{"PII": "yep"}') + user=user, title="old unknown", data='{"PII": "yep"}' + ) # created is auto-set to current time, update bypasses model logic old_date = datetime(2015, 1, 1) @@ -211,16 +202,16 @@ def test_delete_old_data(self): old_unreviewed_dsa.refresh_from_db() assert old_unreviewed_dsa.data is None - assert old_unreviewed_dsa.review == ( - DocumentSpamAttempt.REVIEW_UNAVAILABLE) + assert old_unreviewed_dsa.review == (DocumentSpamAttempt.REVIEW_UNAVAILABLE) def test_delete_logs_for_purged_documents(root_doc, wiki_user): ddl1 = DocumentDeletionLog.objects.create( - locale=root_doc.locale, slug=root_doc.slug, user=wiki_user, - reason='Doomed.') + locale=root_doc.locale, slug=root_doc.slug, user=wiki_user, reason="Doomed." + ) root_doc.delete() # Soft-delete it DocumentDeletionLog.objects.create( - locale='en-US', slug='HardDeleted', user=wiki_user, reason='Purged.') + locale="en-US", slug="HardDeleted", user=wiki_user, reason="Purged." + ) delete_logs_for_purged_documents() assert list(DocumentDeletionLog.objects.all()) == [ddl1] diff --git a/kuma/wiki/tests/test_templates.py b/kuma/wiki/tests/test_templates.py index c19f6587b78..a80106f015a 100644 --- a/kuma/wiki/tests/test_templates.py +++ b/kuma/wiki/tests/test_templates.py @@ -13,17 +13,24 @@ from django.utils import translation from pyquery import PyQuery as pq -from kuma.core.tests import (assert_no_cache_header, - assert_shared_cache_header, - call_on_commit_immediately) +from kuma.core.tests import ( + assert_no_cache_header, + assert_shared_cache_header, + call_on_commit_immediately, +) from kuma.core.urlresolvers import reverse from kuma.core.utils import urlparams from kuma.users.models import User from kuma.users.tests import UserTestCase -from . import (create_topical_parents_docs, document, - new_document_data, revision, WikiTestCase) -from ..constants import (EXPERIMENT_TITLE_PREFIX, REDIRECT_CONTENT) +from . import ( + create_topical_parents_docs, + document, + new_document_data, + revision, + WikiTestCase, +) +from ..constants import EXPERIMENT_TITLE_PREFIX, REDIRECT_CONTENT from ..events import EditDocumentEvent from ..models import Document, Revision @@ -44,12 +51,13 @@ def test_deletion_log_assert(db, rf): """deletion_log.html doesn't render for non-moderators.""" user = AnonymousUser() - request = rf.get('/en-US/docs/DeletedDoc') + request = rf.get("/en-US/docs/DeletedDoc") request.user = user with pytest.raises(RuntimeError) as exc: - render(request, 'wiki/deletion_log.html') - assert str(exc.value) == ('Failed assertion: Deletion log details are only' - ' for moderators.') + render(request, "wiki/deletion_log.html") + assert str(exc.value) == ( + "Failed assertion: Deletion log details are only" " for moderators." + ) class DocumentTests(UserTestCase, WikiTestCase): @@ -57,127 +65,126 @@ class DocumentTests(UserTestCase, WikiTestCase): def test_document_view(self): """Load the document view page and verify the title and content.""" - r = revision(save=True, content='Some text.', is_approved=True) - response = self.client.get(r.document.get_absolute_url(), - HTTP_HOST=settings.WIKI_HOST) + r = revision(save=True, content="Some text.", is_approved=True) + response = self.client.get( + r.document.get_absolute_url(), HTTP_HOST=settings.WIKI_HOST + ) assert 200 == response.status_code doc = pq(response.content) - assert (doc('main#content div.document-head h1').text() == - str(r.document.title)) - assert doc('article#wikiArticle').text() == r.document.html + assert doc("main#content div.document-head h1").text() == str(r.document.title) + assert doc("article#wikiArticle").text() == r.document.html @pytest.mark.breadcrumbs def test_document_no_breadcrumbs(self): """Create docs with topical parent/child rel, verify no breadcrumbs.""" d1, d2 = create_topical_parents_docs() - response = self.client.get(d1.get_absolute_url(), - HTTP_HOST=settings.WIKI_HOST) + response = self.client.get(d1.get_absolute_url(), HTTP_HOST=settings.WIKI_HOST) assert 200 == response.status_code doc = pq(response.content) - assert doc('main#content div.document-head h1').text() == d1.title - assert len(doc('nav.crumbs')) == 0 + assert doc("main#content div.document-head h1").text() == d1.title + assert len(doc("nav.crumbs")) == 0 - response = self.client.get(d2.get_absolute_url(), - HTTP_HOST=settings.WIKI_HOST) + response = self.client.get(d2.get_absolute_url(), HTTP_HOST=settings.WIKI_HOST) assert 200 == response.status_code doc = pq(response.content) - assert doc('main#content div.document-head h1').text() == d2.title - assert len(doc('nav.crumbs')) == 0 + assert doc("main#content div.document-head h1").text() == d2.title + assert len(doc("nav.crumbs")) == 0 @pytest.mark.breadcrumbs def test_document_has_breadcrumbs(self): """Documents with parents and a left column have breadcrumbs.""" d1, d2 = create_topical_parents_docs() - d1.quick_links_html = '
    • Quick Link
    ' + d1.quick_links_html = "
    • Quick Link
    " d1.save() - d2.quick_links_html = '
    • Quick Link
    ' + d2.quick_links_html = "
    • Quick Link
    " d2.save() - response = self.client.get(d1.get_absolute_url(), - HTTP_HOST=settings.WIKI_HOST) + response = self.client.get(d1.get_absolute_url(), HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 doc = pq(response.content) - assert doc('main#content div.document-head h1').text() == d1.title - assert len(doc('nav.crumbs')) == 0 # No parents, no breadcrumbs + assert doc("main#content div.document-head h1").text() == d1.title + assert len(doc("nav.crumbs")) == 0 # No parents, no breadcrumbs - response = self.client.get(d2.get_absolute_url(), - HTTP_HOST=settings.WIKI_HOST) + response = self.client.get(d2.get_absolute_url(), HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 doc = pq(response.content) - assert doc('main#content div.document-head h1').text() == d2.title + assert doc("main#content div.document-head h1").text() == d2.title crumbs = "%s\n%s" % (d1.title, d2.title) - assert doc('nav.crumbs').text() == crumbs + assert doc("nav.crumbs").text() == crumbs def test_english_document_no_approved_content(self): """Load an English document with no approved content.""" - r = revision(save=True, content='Some text.', is_approved=False) - response = self.client.get(r.document.get_absolute_url(), - HTTP_HOST=settings.WIKI_HOST) + r = revision(save=True, content="Some text.", is_approved=False) + response = self.client.get( + r.document.get_absolute_url(), HTTP_HOST=settings.WIKI_HOST + ) assert 200 == response.status_code doc = pq(response.content) - assert doc('main#content div.document-head h1').text() == str(r.document.title) - assert ("This article doesn't have approved content yet." == - doc('article#wikiArticle').text()) + assert doc("main#content div.document-head h1").text() == str(r.document.title) + assert ( + "This article doesn't have approved content yet." + == doc("article#wikiArticle").text() + ) def test_translation_document_no_approved_content(self): """Load a non-English document with no approved content, with a parent with no approved content either.""" - r = revision(save=True, content='Some text.', is_approved=False) - d2 = document(parent=r.document, locale='fr', slug='french', save=True) - revision(document=d2, save=True, content='Moartext', is_approved=False) - response = self.client.get(d2.get_absolute_url(), - HTTP_HOST=settings.WIKI_HOST) + r = revision(save=True, content="Some text.", is_approved=False) + d2 = document(parent=r.document, locale="fr", slug="french", save=True) + revision(document=d2, save=True, content="Moartext", is_approved=False) + response = self.client.get(d2.get_absolute_url(), HTTP_HOST=settings.WIKI_HOST) assert 200 == response.status_code doc = pq(response.content) - assert doc('main#content div.document-head h1').text() == str(d2.title) + assert doc("main#content div.document-head h1").text() == str(d2.title) # HACK: fr doc has different message if locale/ is updated - assert (("This article doesn't have approved content yet." in - doc('article#wikiArticle').text()) or - ("Cet article n'a pas encore de contenu" in - doc('article#wikiArticle').text())) + assert ( + "This article doesn't have approved content yet." + in doc("article#wikiArticle").text() + ) or ( + "Cet article n'a pas encore de contenu" in doc("article#wikiArticle").text() + ) def test_document_fallback_with_translation(self): """The document template falls back to English if translation exists but it has no approved revisions.""" - r = revision(save=True, content='Test', is_approved=True) - d2 = document(parent=r.document, locale='fr', slug='french', save=True) + r = revision(save=True, content="Test", is_approved=True) + d2 = document(parent=r.document, locale="fr", slug="french", save=True) revision(document=d2, is_approved=False, save=True) - url = reverse('wiki.document', args=[d2.slug], locale='fr') + url = reverse("wiki.document", args=[d2.slug], locale="fr") response = self.client.get(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 assert_shared_cache_header(response) doc = pq(response.content) - assert doc('main#content div.document-head h1').text() == str(d2.title) + assert doc("main#content div.document-head h1").text() == str(d2.title) # Fallback message is shown. - assert len(doc('#doc-pending-fallback')) == 1 - assert '$translate' in doc('#edit-button').attr('href') + assert len(doc("#doc-pending-fallback")) == 1 + assert "$translate" in doc("#edit-button").attr("href") # Removing this as it shows up in text(), and we don't want to depend # on its localization. - doc('#doc-pending-fallback').remove() + doc("#doc-pending-fallback").remove() # Included content is English. - assert pq(r.document.html).text() == doc('article#wikiArticle').text() + assert pq(r.document.html).text() == doc("article#wikiArticle").text() def test_document_fallback_no_translation(self): """The document template falls back to English if no translation exists.""" - r = revision(save=True, content='Some text.', is_approved=True) - url = reverse('wiki.document', args=[r.document.slug], locale='fr') + r = revision(save=True, content="Some text.", is_approved=True) + url = reverse("wiki.document", args=[r.document.slug], locale="fr") response = self.client.get(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 assert_shared_cache_header(response) doc = pq(response.content) - assert (doc('main#content div.document-head h1').text() == - str(r.document.title)) + assert doc("main#content div.document-head h1").text() == str(r.document.title) # Fallback message is shown. - assert len(doc('#doc-pending-fallback')) == 1 - assert '$translate' in doc('#edit-button').attr('href') + assert len(doc("#doc-pending-fallback")) == 1 + assert "$translate" in doc("#edit-button").attr("href") # Removing this as it shows up in text(), and we don't want to depend # on its localization. - doc('#doc-pending-fallback').remove() + doc("#doc-pending-fallback").remove() # Included content is English. - assert pq(r.document.html).text() == doc('article#wikiArticle').text() + assert pq(r.document.html).text() == doc("article#wikiArticle").text() def test_redirect(self): """Make sure documents with REDIRECT directives redirect properly. @@ -189,14 +196,15 @@ def test_redirect(self): # Ordinarily, a document with no approved revisions cannot have HTML, # but we shove it in manually here as a shortcut: - redirect_html = REDIRECT_CONTENT % dict(title='Boo', href=target_url) + redirect_html = REDIRECT_CONTENT % dict(title="Boo", href=target_url) redirect = document(html=redirect_html) redirect.save() redirect_url = redirect.get_absolute_url() - self.client.login(username='admin', password='testpass') - response = self.client.get(redirect_url, follow=True, - HTTP_HOST=settings.WIKI_HOST) + self.client.login(username="admin", password="testpass") + response = self.client.get( + redirect_url, follow=True, HTTP_HOST=settings.WIKI_HOST + ) self.assertRedirects(response, urlparams(target_url), status_code=301) self.assertContains(response, redirect_url) @@ -204,28 +212,25 @@ def test_redirect_from_nonexistent(self): """The template shouldn't crash or print a backlink if the "from" page doesn't exist.""" d = document(save=True) - response = self.client.get(urlparams(d.get_absolute_url()), - HTTP_HOST=settings.WIKI_HOST) - self.assertNotContains(response, 'Redirected from ') + response = self.client.get( + urlparams(d.get_absolute_url()), HTTP_HOST=settings.WIKI_HOST + ) + self.assertNotContains(response, "Redirected from ") def test_non_localizable_translate_disabled(self): """Non localizable document doesn't show tab for 'Localize'.""" - self.client.login(username='testuser', password='testpass') + self.client.login(username="testuser", password="testpass") d = document(is_localizable=True, save=True) - resp = self.client.get(d.get_absolute_url(), - HTTP_HOST=settings.WIKI_HOST) + resp = self.client.get(d.get_absolute_url(), HTTP_HOST=settings.WIKI_HOST) doc = pq(resp.content) - assert ('Add a translation' in - doc('.page-buttons #translations li').text()) + assert "Add a translation" in doc(".page-buttons #translations li").text() # Make it non-localizable d.is_localizable = False d.save() - resp = self.client.get(d.get_absolute_url(), - HTTP_HOST=settings.WIKI_HOST) + resp = self.client.get(d.get_absolute_url(), HTTP_HOST=settings.WIKI_HOST) doc = pq(resp.content) - assert ('Add a translation' not in - doc('.page-buttons #translations li').text()) + assert "Add a translation" not in doc(".page-buttons #translations li").text() @pytest.mark.toc def test_toc_depth(self): @@ -238,15 +243,18 @@ def test_toc_depth(self):

    This is more section content.

    """ r = revision(save=True, content=doc_content, is_approved=True) - response = self.client.get(r.document.get_absolute_url(), - HTTP_HOST=settings.WIKI_HOST) + response = self.client.get( + r.document.get_absolute_url(), HTTP_HOST=settings.WIKI_HOST + ) assert 200 == response.status_code assert b'
    ' not in response.content @@ -258,8 +266,9 @@ def test_lang_switcher_footer(self): trans_pt_br = document(parent=parent, locale="pt-BR", save=True) trans_fr = document(parent=parent, locale="fr", save=True) - response = self.client.get(trans_pt_br.get_absolute_url(), - HTTP_HOST=settings.WIKI_HOST) + response = self.client.get( + trans_pt_br.get_absolute_url(), HTTP_HOST=settings.WIKI_HOST + ) assert 200 == response.status_code doc = pq(response.content) options = doc(".languages.go select.wiki-l10n option") @@ -282,8 +291,9 @@ def test_lang_switcher_button(self): trans_pt_br = document(parent=parent, locale="pt-BR", save=True) trans_fr = document(parent=parent, locale="fr", save=True) - response = self.client.get(trans_pt_br.get_absolute_url(), - HTTP_HOST=settings.WIKI_HOST) + response = self.client.get( + trans_pt_br.get_absolute_url(), HTTP_HOST=settings.WIKI_HOST + ) assert 200 == response.status_code doc = pq(response.content) options = doc("#languages-menu-submenu ul#translations li a") @@ -298,51 +308,54 @@ def test_lang_switcher_button(self): assert trans_fr.language in options[3].text def test_experiment_document_view(self): - slug = EXPERIMENT_TITLE_PREFIX + 'Test' - r = revision(save=True, content='Experiment.', is_approved=True, - slug=slug) + slug = EXPERIMENT_TITLE_PREFIX + "Test" + r = revision(save=True, content="Experiment.", is_approved=True, slug=slug) assert r.document.is_experiment - response = self.client.get(r.document.get_absolute_url(), - HTTP_HOST=settings.WIKI_HOST) + response = self.client.get( + r.document.get_absolute_url(), HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 200 doc = pq(response.content) - doc_title = doc('main#content div.document-head h1').text() + doc_title = doc("main#content div.document-head h1").text() assert doc_title == str(r.document.title) - assert doc('article#wikiArticle').text() == r.document.html + assert doc("article#wikiArticle").text() == r.document.html metas = doc("meta[name='robots']") assert len(metas) == 1 - meta_content = metas[0].get('content') - assert meta_content == 'noindex, nofollow' - doc_experiment = doc('div#doc-experiment') + meta_content = metas[0].get("content") + assert meta_content == "noindex, nofollow" + doc_experiment = doc("div#doc-experiment") assert len(doc_experiment) == 1 -_TEST_CONTENT_EXPERIMENTS = [{ - 'id': 'experiment-test', - 'ga_name': 'experiment-test', - 'param': 'v', - 'pages': { - 'en-US:Original': { - 'control': 'Original', - 'test': 'Experiment:Test/Variant', - } +_TEST_CONTENT_EXPERIMENTS = [ + { + "id": "experiment-test", + "ga_name": "experiment-test", + "param": "v", + "pages": { + "en-US:Original": { + "control": "Original", + "test": "Experiment:Test/Variant", + } + }, } -}] +] _PIPELINE = settings.PIPELINE -_PIPELINE['JAVASCRIPT']['experiment-test'] = { - 'output_filename': 'build/js/experiment-framework-test.js', +_PIPELINE["JAVASCRIPT"]["experiment-test"] = { + "output_filename": "build/js/experiment-framework-test.js", } -@override_settings(CONTENT_EXPERIMENTS=_TEST_CONTENT_EXPERIMENTS, - PIPELINE=_PIPELINE, - GOOGLE_ANALYTICS_ACCOUNT='fake') +@override_settings( + CONTENT_EXPERIMENTS=_TEST_CONTENT_EXPERIMENTS, + PIPELINE=_PIPELINE, + GOOGLE_ANALYTICS_ACCOUNT="fake", +) class DocumentContentExperimentTests(UserTestCase, WikiTestCase): # src attribute of the content experiment ' - % settings.STATIC_URL) + "Root Document - sample1 - code sample" + "Some HTML" + '' % settings.STATIC_URL + ) assert normalized == expected def test_code_sample_host_not_allowed(code_sample_doc, settings, client): """Users are not allowed to view samples on a restricted domain.""" - url = reverse('wiki.code_sample', - args=[code_sample_doc.slug, 'sample1']) - host = 'testserver' + url = reverse("wiki.code_sample", args=[code_sample_doc.slug, "sample1"]) + host = "testserver" assert settings.ATTACHMENT_HOST != host assert settings.ATTACHMENT_ORIGIN != host response = client.get(url, HTTP_HOST=host) @@ -75,23 +70,22 @@ def test_code_sample_host_not_allowed(code_sample_doc, settings, client): def test_code_sample_host_allowed(code_sample_doc, settings, client): """Users are allowed to view samples on an allowed domain.""" - host = 'sampleserver' - url = reverse('wiki.code_sample', - args=[code_sample_doc.slug, 'sample1']) + host = "sampleserver" + url = reverse("wiki.code_sample", args=[code_sample_doc.slug, "sample1"]) settings.ATTACHMENT_HOST = host settings.ALLOWED_HOSTS.append(host) response = client.get(url, HTTP_HOST=host) assert response.status_code == 200 - assert 'public' in response['Cache-Control'] - assert 'max-age=86400' in response['Cache-Control'] + assert "public" in response["Cache-Control"] + assert "max-age=86400" in response["Cache-Control"] -def test_code_sample_host_restricted_host(code_sample_doc, constance_config, - settings, client): +def test_code_sample_host_restricted_host( + code_sample_doc, constance_config, settings, client +): """Users are allowed to view samples on the attachment domain.""" - url = reverse('wiki.code_sample', - args=[code_sample_doc.slug, 'sample1']) - host = 'sampleserver' + url = reverse("wiki.code_sample", args=[code_sample_doc.slug, "sample1"]) + host = "sampleserver" settings.ALLOWED_HOSTS.append(host) settings.ATTACHMENT_HOST = host settings.ENABLE_RESTRICTIONS_BY_HOST = True @@ -101,62 +95,67 @@ def test_code_sample_host_restricted_host(code_sample_doc, constance_config, constance_config.KUMASCRIPT_TIMEOUT = 1 response = client.get(url, HTTP_HOST=host) assert response.status_code == 200 - assert 'public' in response['Cache-Control'] - assert 'max-age=86400' in response['Cache-Control'] + assert "public" in response["Cache-Control"] + assert "max-age=86400" in response["Cache-Control"] -def test_raw_code_sample_file(code_sample_doc, constance_config, - wiki_user, admin_client, settings): +def test_raw_code_sample_file( + code_sample_doc, constance_config, wiki_user, admin_client, settings +): # Upload an attachment - upload_url = reverse('attachments.edit_attachment', - kwargs={'document_path': code_sample_doc.slug}) - file_for_upload = make_test_file(content='Something something unique') + upload_url = reverse( + "attachments.edit_attachment", kwargs={"document_path": code_sample_doc.slug} + ) + file_for_upload = make_test_file(content="Something something unique") post_data = { - 'title': 'An uploaded file', - 'description': 'A unique experience for your file serving needs.', - 'comment': 'Yadda yadda yadda', - 'file': file_for_upload, + "title": "An uploaded file", + "description": "A unique experience for your file serving needs.", + "comment": "Yadda yadda yadda", + "file": file_for_upload, } - constance_config.WIKI_ATTACHMENT_ALLOWED_TYPES = 'text/plain' - response = admin_client.post(upload_url, data=post_data, - HTTP_HOST=settings.WIKI_HOST) + constance_config.WIKI_ATTACHMENT_ALLOWED_TYPES = "text/plain" + response = admin_client.post( + upload_url, data=post_data, HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 302 - edit_url = reverse('wiki.edit', args=(code_sample_doc.slug,)) + edit_url = reverse("wiki.edit", args=(code_sample_doc.slug,)) assert response.url == edit_url # Add a relative reference to the sample content - attachment = Attachment.objects.get(title='An uploaded file') + attachment = Attachment.objects.get(title="An uploaded file") filename = attachment.current_revision.filename url_css = 'url("files/%(attachment_id)s/%(filename)s")' % { - 'attachment_id': attachment.id, - 'filename': filename, + "attachment_id": attachment.id, + "filename": filename, } new_content = code_sample_doc.current_revision.content.replace( - 'color: red', url_css) + "color: red", url_css + ) code_sample_doc.current_revision = Revision.objects.create( - document=code_sample_doc, creator=wiki_user, content=new_content) + document=code_sample_doc, creator=wiki_user, content=new_content + ) code_sample_doc.save() # URL is in the sample - sample_url = reverse('wiki.code_sample', - args=[code_sample_doc.slug, 'sample1']) + sample_url = reverse("wiki.code_sample", args=[code_sample_doc.slug, "sample1"]) - settings.ATTACHMENT_HOST = 'testserver' + settings.ATTACHMENT_HOST = "testserver" response = admin_client.get(sample_url) assert response.status_code == 200 assert url_css.encode() in response.content - assert 'public' in response['Cache-Control'] - assert 'max-age=86400' in response['Cache-Control'] + assert "public" in response["Cache-Control"] + assert "max-age=86400" in response["Cache-Control"] # Getting the URL redirects to the attachment - file_url = reverse('wiki.raw_code_sample_file', - args=(code_sample_doc.slug, 'sample1', attachment.id, - filename)) + file_url = reverse( + "wiki.raw_code_sample_file", + args=(code_sample_doc.slug, "sample1", attachment.id, filename), + ) response = admin_client.get(file_url) assert response.status_code == 302 assert response.url == attachment.get_file_url() - assert not response.has_header('Vary') - assert 'Cache-Control' in response - assert 'public' in response['Cache-Control'] - assert 'max-age=432000' in response['Cache-Control'] + assert not response.has_header("Vary") + assert "Cache-Control" in response + assert "public" in response["Cache-Control"] + assert "max-age=432000" in response["Cache-Control"] diff --git a/kuma/wiki/tests/test_views_create.py b/kuma/wiki/tests/test_views_create.py index 6adbec1d269..d5c3f9d125c 100644 --- a/kuma/wiki/tests/test_views_create.py +++ b/kuma/wiki/tests/test_views_create.py @@ -1,5 +1,3 @@ - - import pytest from django.conf import settings from django.contrib.auth.models import Permission @@ -13,49 +11,49 @@ # dict of case-name --> tuple of slug and expected status code SLUG_SIMPLE_CASES = { - 'invalid_slash': 'Foo/bar', - 'invalid_dollar_sign': 'Foo$bar', - 'invalid_question_mark': 'Foo?bar', - 'invalid_percent_sign': 'Foo%bar', - 'invalid_double_quote': 'Foo"bar', - 'invalid_single_quote': "Foo'bar", - 'invalid_whitespace': 'Foo bar', + "invalid_slash": "Foo/bar", + "invalid_dollar_sign": "Foo$bar", + "invalid_question_mark": "Foo?bar", + "invalid_percent_sign": "Foo%bar", + "invalid_double_quote": 'Foo"bar', + "invalid_single_quote": "Foo'bar", + "invalid_whitespace": "Foo bar", } SLUG_RESERVED_CASES = { - 'invalid_reserved_01': 'ckeditor_config.js', - 'invalid_reserved_02': 'preview-wiki-content', - 'invalid_reserved_03': 'get-documents', - 'invalid_reserved_04': 'tags', - 'invalid_reserved_05': 'tag/editorial', - 'invalid_reserved_06': 'new', - 'invalid_reserved_07': 'all', - 'invalid_reserved_08': 'with-errors', - 'invalid_reserved_09': 'without-parent', - 'invalid_reserved_10': 'top-level', - 'invalid_reserved_11': 'needs-review', - 'invalid_reserved_12': 'needs-review/technical', - 'invalid_reserved_13': 'localization-tag', - 'invalid_reserved_14': 'localization-tag/inprogress', - 'invalid_reserved_15': 'templates', - 'invalid_reserved_16': 'submit_akismet_spam', - 'invalid_reserved_17': 'feeds/atom/all', - 'invalid_reserved_18': 'feeds/rss/all', - 'invalid_reserved_19': 'feeds/atom/l10n-updates', - 'invalid_reserved_20': 'feeds/rss/l10n-updates', - 'invalid_reserved_21': 'feeds/atom/tag/editorial', - 'invalid_reserved_22': 'feeds/atom/needs-review', - 'invalid_reserved_23': 'feeds/rss/needs-review', - 'invalid_reserved_24': 'feeds/atom/needs-review/technical', - 'invalid_reserved_25': 'feeds/atom/revisions', - 'invalid_reserved_26': 'feeds/rss/revisions', - 'invalid_reserved_27': 'feeds/atom/files', - 'invalid_reserved_28': 'feeds/rss/files', + "invalid_reserved_01": "ckeditor_config.js", + "invalid_reserved_02": "preview-wiki-content", + "invalid_reserved_03": "get-documents", + "invalid_reserved_04": "tags", + "invalid_reserved_05": "tag/editorial", + "invalid_reserved_06": "new", + "invalid_reserved_07": "all", + "invalid_reserved_08": "with-errors", + "invalid_reserved_09": "without-parent", + "invalid_reserved_10": "top-level", + "invalid_reserved_11": "needs-review", + "invalid_reserved_12": "needs-review/technical", + "invalid_reserved_13": "localization-tag", + "invalid_reserved_14": "localization-tag/inprogress", + "invalid_reserved_15": "templates", + "invalid_reserved_16": "submit_akismet_spam", + "invalid_reserved_17": "feeds/atom/all", + "invalid_reserved_18": "feeds/rss/all", + "invalid_reserved_19": "feeds/atom/l10n-updates", + "invalid_reserved_20": "feeds/rss/l10n-updates", + "invalid_reserved_21": "feeds/atom/tag/editorial", + "invalid_reserved_22": "feeds/atom/needs-review", + "invalid_reserved_23": "feeds/rss/needs-review", + "invalid_reserved_24": "feeds/atom/needs-review/technical", + "invalid_reserved_25": "feeds/atom/revisions", + "invalid_reserved_26": "feeds/rss/revisions", + "invalid_reserved_27": "feeds/atom/files", + "invalid_reserved_28": "feeds/rss/files", } @pytest.fixture def permission_add_document(db): - return Permission.objects.get(codename='add_document') + return Permission.objects.get(codename="add_document") @pytest.fixture @@ -65,183 +63,183 @@ def add_doc_client(editor_client, wiki_user, permission_add_document): def test_check_read_only_mode(user_client): - response = user_client.get(reverse('wiki.create'), - HTTP_HOST=settings.WIKI_HOST) + response = user_client.get(reverse("wiki.create"), HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 403 assert_no_cache_header(response) def test_user_add_document_permission(editor_client): - response = editor_client.get(reverse('wiki.create'), - HTTP_HOST=settings.WIKI_HOST) + response = editor_client.get(reverse("wiki.create"), HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 403 - assert response['X-Robots-Tag'] == 'noindex' + assert response["X-Robots-Tag"] == "noindex" assert_no_cache_header(response) @pytest.mark.toc def test_get(add_doc_client): - response = add_doc_client.get(reverse('wiki.create'), - HTTP_HOST=settings.WIKI_HOST) + response = add_doc_client.get(reverse("wiki.create"), HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 - assert response['X-Robots-Tag'] == 'noindex' + assert response["X-Robots-Tag"] == "noindex" assert_no_cache_header(response) page = pq(response.content) - toc_select = page.find('#id_toc_depth') - toc_options = toc_select.find('option') + toc_select = page.find("#id_toc_depth") + toc_options = toc_select.find("option") found_selected = False for option in toc_options: opt_element = pq(option) - if opt_element.attr('selected'): + if opt_element.attr("selected"): found_selected = True - assert opt_element.attr('value') == str(Revision.TOC_DEPTH_H4) - assert found_selected, 'No ToC depth initially selected.' + assert opt_element.attr("value") == str(Revision.TOC_DEPTH_H4) + assert found_selected, "No ToC depth initially selected." # Check discard button. - assert (page.find('.btn-discard').attr('href') == reverse('wiki.create')) + assert page.find(".btn-discard").attr("href") == reverse("wiki.create") @pytest.mark.tags @pytest.mark.review_tags def test_create_valid(add_doc_client): """Test creating a new document with valid and invalid slugs.""" - slug = 'Foobar' + slug = "Foobar" data = dict( - title='A Foobar Document', + title="A Foobar Document", slug=slug, - tags='tag1, tag2', - review_tags=['editorial', 'technical'], - keywords='key1, key2', - summary='lipsum', - content='lorem ipsum dolor sit amet', - comment='This is foobar.', + tags="tag1, tag2", + review_tags=["editorial", "technical"], + keywords="key1, key2", + summary="lipsum", + content="lorem ipsum dolor sit amet", + comment="This is foobar.", toc_depth=1, ) - url = reverse('wiki.create') + url = reverse("wiki.create") resp = add_doc_client.post(url, data, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 302 - assert resp['X-Robots-Tag'] == 'noindex' + assert resp["X-Robots-Tag"] == "noindex" assert_no_cache_header(resp) - assert resp['Location'].endswith(reverse('wiki.document', args=(slug,))) + assert resp["Location"].endswith(reverse("wiki.document", args=(slug,))) doc = Document.objects.get(slug=slug) - for name in (set(data.keys()) - {'tags', 'review_tags'}): + for name in set(data.keys()) - {"tags", "review_tags"}: assert getattr(doc.current_revision, name) == data[name] - assert (sorted(doc.tags.all().values_list('name', flat=True)) == - ['tag1', 'tag2']) + assert sorted(doc.tags.all().values_list("name", flat=True)) == ["tag1", "tag2"] review_tags = doc.current_revision.review_tags - assert (sorted(review_tags.all().values_list('name', flat=True)) == - ['editorial', 'technical']) + assert sorted(review_tags.all().values_list("name", flat=True)) == [ + "editorial", + "technical", + ] @pytest.mark.tags @pytest.mark.review_tags @pytest.mark.parametrize( - 'slug', + "slug", list(SLUG_SIMPLE_CASES.values()) + list(SLUG_RESERVED_CASES.values()), - ids=list(SLUG_SIMPLE_CASES) + list(SLUG_RESERVED_CASES)) + ids=list(SLUG_SIMPLE_CASES) + list(SLUG_RESERVED_CASES), +) def test_create_invalid(add_doc_client, slug): """Test creating a new document with valid and invalid slugs.""" data = dict( - title='A Foobar Document', + title="A Foobar Document", slug=slug, - tags='tag1, tag2', - review_tags=['editorial', 'technical'], - keywords='key1, key2', - summary='lipsum', - content='lorem ipsum dolor sit amet', - comment='This is foobar.', + tags="tag1, tag2", + review_tags=["editorial", "technical"], + keywords="key1, key2", + summary="lipsum", + content="lorem ipsum dolor sit amet", + comment="This is foobar.", toc_depth=1, ) - url = reverse('wiki.create') + url = reverse("wiki.create") resp = add_doc_client.post(url, data, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 200 - assert resp['X-Robots-Tag'] == 'noindex' + assert resp["X-Robots-Tag"] == "noindex" assert_no_cache_header(resp) - assert b'The slug provided is not valid.' in resp.content + assert b"The slug provided is not valid." in resp.content with pytest.raises(Document.DoesNotExist): - Document.objects.get(slug=slug, locale='en-US') - assert pq(resp.content).find('input[name=slug]')[0].value == slug + Document.objects.get(slug=slug, locale="en-US") + assert pq(resp.content).find("input[name=slug]")[0].value == slug @pytest.mark.tags @pytest.mark.review_tags -@pytest.mark.parametrize('slug', ['Foobar', 'Root']) +@pytest.mark.parametrize("slug", ["Foobar", "Root"]) def test_create_child_valid(root_doc, add_doc_client, slug): """Test creating a new child document with valid and invalid slugs.""" data = dict( - title='A Child of the Root Document', + title="A Child of the Root Document", slug=slug, - tags='tag1, tag2', - review_tags=['editorial', 'technical'], - keywords='key1, key2', - summary='lipsum', - content='lorem ipsum dolor sit amet', - comment='This is foobar.', + tags="tag1, tag2", + review_tags=["editorial", "technical"], + keywords="key1, key2", + summary="lipsum", + content="lorem ipsum dolor sit amet", + comment="This is foobar.", toc_depth=1, ) - url = reverse('wiki.create') - url += '?parent={}'.format(root_doc.id) - full_slug = '{}/{}'.format(root_doc.slug, slug) + url = reverse("wiki.create") + url += "?parent={}".format(root_doc.id) + full_slug = "{}/{}".format(root_doc.slug, slug) resp = add_doc_client.post(url, data, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 302 - assert resp['X-Robots-Tag'] == 'noindex' + assert resp["X-Robots-Tag"] == "noindex" assert_no_cache_header(resp) - assert resp['Location'].endswith( - reverse('wiki.document', args=(full_slug,))) + assert resp["Location"].endswith(reverse("wiki.document", args=(full_slug,))) assert root_doc.children.count() == 1 - doc = Document.objects.get(slug=full_slug, locale='en-US') - skip_keys = {'tags', 'review_tags', 'parent_topic'} - for name in (set(data.keys()) - skip_keys): - expected = full_slug if name == 'slug' else data[name] + doc = Document.objects.get(slug=full_slug, locale="en-US") + skip_keys = {"tags", "review_tags", "parent_topic"} + for name in set(data.keys()) - skip_keys: + expected = full_slug if name == "slug" else data[name] assert getattr(doc.current_revision, name) == expected - assert (sorted(doc.tags.all().values_list('name', flat=True)) == - ['tag1', 'tag2']) + assert sorted(doc.tags.all().values_list("name", flat=True)) == ["tag1", "tag2"] review_tags = doc.current_revision.review_tags - assert (sorted(review_tags.all().values_list('name', flat=True)) == - ['editorial', 'technical']) + assert sorted(review_tags.all().values_list("name", flat=True)) == [ + "editorial", + "technical", + ] @pytest.mark.tags @pytest.mark.review_tags @pytest.mark.parametrize( - 'slug', - list(SLUG_SIMPLE_CASES.values()), - ids=list(SLUG_SIMPLE_CASES)) + "slug", list(SLUG_SIMPLE_CASES.values()), ids=list(SLUG_SIMPLE_CASES) +) def test_create_child_invalid(root_doc, add_doc_client, slug): """Test creating a new child document with valid and invalid slugs.""" data = dict( - title='A Child of the Root Document', + title="A Child of the Root Document", slug=slug, - tags='tag1, tag2', - review_tags=['editorial', 'technical'], - keywords='key1, key2', - summary='lipsum', - content='lorem ipsum dolor sit amet', - comment='This is foobar.', + tags="tag1, tag2", + review_tags=["editorial", "technical"], + keywords="key1, key2", + summary="lipsum", + content="lorem ipsum dolor sit amet", + comment="This is foobar.", toc_depth=1, ) - url = reverse('wiki.create') - url += '?parent={}'.format(root_doc.id) - full_slug = '{}/{}'.format(root_doc.slug, slug) + url = reverse("wiki.create") + url += "?parent={}".format(root_doc.id) + full_slug = "{}/{}".format(root_doc.slug, slug) resp = add_doc_client.post(url, data, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 200 - assert resp['X-Robots-Tag'] == 'noindex' + assert resp["X-Robots-Tag"] == "noindex" assert_no_cache_header(resp) - assert b'The slug provided is not valid.' in resp.content + assert b"The slug provided is not valid." in resp.content with pytest.raises(Document.DoesNotExist): - Document.objects.get(slug=full_slug, locale='en-US') + Document.objects.get(slug=full_slug, locale="en-US") page = pq(resp.content) - assert page.find('input[name=slug]')[0].value == slug + assert page.find("input[name=slug]")[0].value == slug def test_clone_get(root_doc, add_doc_client): - url = reverse('wiki.create') - url += '?clone={}'.format(root_doc.id) + url = reverse("wiki.create") + url += "?clone={}".format(root_doc.id) response = add_doc_client.get(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 - assert response['X-Robots-Tag'] == 'noindex' + assert response["X-Robots-Tag"] == "noindex" assert_no_cache_header(response) page = pq(response.content) - assert page.find('input[name=slug]')[0].value is None - assert page.find('input[name=title]')[0].value is None - assert (page.find('textarea[name=content]')[0].value.strip() == - root_doc.current_revision.content) + assert page.find("input[name=slug]")[0].value is None + assert page.find("input[name=title]")[0].value is None + assert ( + page.find("textarea[name=content]")[0].value.strip() + == root_doc.current_revision.content + ) diff --git a/kuma/wiki/tests/test_views_delete.py b/kuma/wiki/tests/test_views_delete.py index 3f602af9205..7e7488413c4 100644 --- a/kuma/wiki/tests/test_views_delete.py +++ b/kuma/wiki/tests/test_views_delete.py @@ -1,5 +1,3 @@ - - import pytest from django.conf import settings @@ -9,115 +7,132 @@ from ..models import Document, DocumentDeletionLog -@pytest.mark.parametrize('endpoint', ['revert_document', 'delete_document', - 'restore_document', 'purge_document']) +@pytest.mark.parametrize( + "endpoint", + ["revert_document", "delete_document", "restore_document", "purge_document"], +) def test_login(root_doc, client, endpoint): """Tests that login is required. The "client" fixture is not logged in.""" args = [root_doc.slug] - if endpoint == 'revert_document': + if endpoint == "revert_document": args.append(root_doc.current_revision.id) - url = reverse('wiki.{}'.format(endpoint), args=args) + url = reverse("wiki.{}".format(endpoint), args=args) response = client.get(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 302 - assert 'en-US/users/signin?' in response['Location'] + assert "en-US/users/signin?" in response["Location"] assert_no_cache_header(response) @pytest.mark.parametrize( - 'endpoint', ['delete_document', 'restore_document', 'purge_document']) + "endpoint", ["delete_document", "restore_document", "purge_document"] +) def test_permission(root_doc, editor_client, endpoint): """ Tests that the proper permission is required. The "editor_client" fixture, although logged in, does not have the proper permission. """ args = [root_doc.slug] - url = reverse('wiki.{}'.format(endpoint), args=args) + url = reverse("wiki.{}".format(endpoint), args=args) response = editor_client.get(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 403 assert_no_cache_header(response) -@pytest.mark.parametrize('endpoint', ['revert_document', 'delete_document', - 'restore_document', 'purge_document']) +@pytest.mark.parametrize( + "endpoint", + ["revert_document", "delete_document", "restore_document", "purge_document"], +) def test_read_only_mode(root_doc, user_client, endpoint): args = [root_doc.slug] - if endpoint == 'revert_document': + if endpoint == "revert_document": args.append(root_doc.current_revision.id) - url = reverse('wiki.{}'.format(endpoint), args=args) + url = reverse("wiki.{}".format(endpoint), args=args) response = user_client.get(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 403 assert_no_cache_header(response) def test_delete_get(root_doc, moderator_client): - url = reverse('wiki.delete_document', args=[root_doc.slug]) + url = reverse("wiki.delete_document", args=[root_doc.slug]) response = moderator_client.get(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 assert_no_cache_header(response) def test_purge_get(deleted_doc, moderator_client): - url = reverse('wiki.purge_document', args=[deleted_doc.slug]) + url = reverse("wiki.purge_document", args=[deleted_doc.slug]) response = moderator_client.get(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 assert_no_cache_header(response) - assert ('This document was deleted by' in - response.content.decode(response.charset)) + assert "This document was deleted by" in response.content.decode(response.charset) def test_purge_get_no_log(deleted_doc, moderator_client): - url = reverse('wiki.purge_document', args=[deleted_doc.slug]) + url = reverse("wiki.purge_document", args=[deleted_doc.slug]) DocumentDeletionLog.objects.all().delete() response = moderator_client.get(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 assert_no_cache_header(response) - assert ('deleted, for unknown reasons' in - response.content.decode(response.charset)) + assert "deleted, for unknown reasons" in response.content.decode(response.charset) def test_restore_get(root_doc, moderator_client): root_doc.delete() with pytest.raises(Document.DoesNotExist): Document.objects.get(slug=root_doc.slug, locale=root_doc.locale) - url = reverse('wiki.restore_document', args=[root_doc.slug]) + url = reverse("wiki.restore_document", args=[root_doc.slug]) response = moderator_client.get(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 302 - assert response['Location'].endswith(root_doc.get_absolute_url()) + assert response["Location"].endswith(root_doc.get_absolute_url()) assert_no_cache_header(response) assert Document.objects.get(slug=root_doc.slug, locale=root_doc.locale) def test_revert_get(root_doc, moderator_client): - url = reverse('wiki.revert_document', - args=[root_doc.slug, root_doc.current_revision.id]) + url = reverse( + "wiki.revert_document", args=[root_doc.slug, root_doc.current_revision.id] + ) response = moderator_client.get(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 assert_no_cache_header(response) def test_delete_post(root_doc, moderator_client): - url = reverse('wiki.delete_document', args=[root_doc.slug]) - response = moderator_client.post(url, data=dict(reason='test'), - HTTP_HOST=settings.WIKI_HOST) + url = reverse("wiki.delete_document", args=[root_doc.slug]) + response = moderator_client.post( + url, data=dict(reason="test"), HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 302 - assert response['Location'].endswith(root_doc.get_absolute_url()) + assert response["Location"].endswith(root_doc.get_absolute_url()) assert_no_cache_header(response) - assert len(Document.admin_objects.filter( - slug=root_doc.slug, locale=root_doc.locale, deleted=True)) == 1 + assert ( + len( + Document.admin_objects.filter( + slug=root_doc.slug, locale=root_doc.locale, deleted=True + ) + ) + == 1 + ) with pytest.raises(Document.DoesNotExist): Document.objects.get(slug=root_doc.slug, locale=root_doc.locale) - assert len(DocumentDeletionLog.objects.filter(locale=root_doc.locale, - slug=root_doc.slug, - reason='test')) == 1 + assert ( + len( + DocumentDeletionLog.objects.filter( + locale=root_doc.locale, slug=root_doc.slug, reason="test" + ) + ) + == 1 + ) def test_purge_post(root_doc, moderator_client): root_doc.delete() - url = reverse('wiki.purge_document', args=[root_doc.slug]) - response = moderator_client.post(url, data=dict(confirm='true'), - HTTP_HOST=settings.WIKI_HOST) + url = reverse("wiki.purge_document", args=[root_doc.slug]) + response = moderator_client.post( + url, data=dict(confirm="true"), HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 302 - assert response['Location'].endswith(root_doc.get_absolute_url()) + assert response["Location"].endswith(root_doc.get_absolute_url()) assert_no_cache_header(response) with pytest.raises(Document.DoesNotExist): Document.admin_objects.get(slug=root_doc.slug, locale=root_doc.locale) @@ -127,13 +142,14 @@ def test_revert_post(edit_revision, moderator_client): root_doc = edit_revision.document assert len(root_doc.revisions.all()) == 2 first_revision = root_doc.revisions.first() - url = reverse('wiki.revert_document', - args=[root_doc.slug, first_revision.id]) - response = moderator_client.post(url, data=dict(comment='test'), - HTTP_HOST=settings.WIKI_HOST) + url = reverse("wiki.revert_document", args=[root_doc.slug, first_revision.id]) + response = moderator_client.post( + url, data=dict(comment="test"), HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 302 - assert response['Location'].endswith(reverse('wiki.document_revisions', - args=[root_doc.slug])) + assert response["Location"].endswith( + reverse("wiki.document_revisions", args=[root_doc.slug]) + ) assert_no_cache_header(response) assert len(root_doc.revisions.all()) == 3 root_doc.refresh_from_db() diff --git a/kuma/wiki/tests/test_views_document.py b/kuma/wiki/tests/test_views_document.py index 4319169f5c9..d4b6c3644aa 100644 --- a/kuma/wiki/tests/test_views_document.py +++ b/kuma/wiki/tests/test_views_document.py @@ -23,8 +23,11 @@ from kuma.authkeys.models import Key from kuma.core.models import IPBan -from kuma.core.tests import (assert_no_cache_header, assert_redirect_to_wiki, - assert_shared_cache_header) +from kuma.core.tests import ( + assert_no_cache_header, + assert_redirect_to_wiki, + assert_shared_cache_header, +) from kuma.core.urlresolvers import reverse from . import HREFLANG_TEST_CASES @@ -35,41 +38,45 @@ from ..views.utils import calculate_etag -AuthKey = namedtuple('AuthKey', 'key header') +AuthKey = namedtuple("AuthKey", "key header") -EMPTY_IFRAME = '' +EMPTY_IFRAME = "" SECTION1 = '

    S1

    This is a page. Deal with it.

    ' SECTION2 = '

    S2

    This is a page. Deal with it.

    ' SECTION3 = '

    S3

    This is a page. Deal with it.

    ' SECTION4 = '

    S4

    This is a page. Deal with it.

    ' SECTIONS = SECTION1 + SECTION2 + SECTION3 + SECTION4 SECTION_CASE_TO_DETAILS = { - 'no-section': (None, SECTIONS), - 'section': ('S1', SECTION1), - 'another-section': ('S3', SECTION3), - 'non-existent-section': ('S99', '') + "no-section": (None, SECTIONS), + "section": ("S1", SECTION1), + "another-section": ("S3", SECTION3), + "non-existent-section": ("S99", ""), } def get_content(content_case, data): - if content_case == 'multipart': + if content_case == "multipart": return MULTIPART_CONTENT, encode_multipart(BOUNDARY, data) - if content_case == 'json': - return 'application/json', json.dumps(data) + if content_case == "json": + return "application/json", json.dumps(data) - if content_case == 'html-fragment': - return 'text/html', data['content'] + if content_case == "html-fragment": + return "text/html", data["content"] - assert content_case == 'html' - return 'text/html', """ + assert content_case == "html" + return ( + "text/html", + """ %(title)s %(content)s - """ % data + """ + % data, + ) @pytest.fixture @@ -81,60 +88,60 @@ def section_doc(root_doc, wiki_user): the GZipMiddleware, if used. """ root_doc.current_revision = Revision.objects.create( - document=root_doc, creator=wiki_user, content=SECTIONS) + document=root_doc, creator=wiki_user, content=SECTIONS + ) root_doc.save() return root_doc @pytest.fixture def ce_settings(settings): - settings.CONTENT_EXPERIMENTS = [{ - 'id': 'experiment-test', - 'ga_name': 'experiment-test', - 'param': 'v', - 'pages': { - 'en-US:Original': { - 'control': 'Original', - 'test': 'Experiment:Test/Variant', - } + settings.CONTENT_EXPERIMENTS = [ + { + "id": "experiment-test", + "ga_name": "experiment-test", + "param": "v", + "pages": { + "en-US:Original": { + "control": "Original", + "test": "Experiment:Test/Variant", + } + }, } - }] + ] return settings @pytest.fixture def authkey(wiki_user): - key = Key(user=wiki_user, description='Test Key 1') + key = Key(user=wiki_user, description="Test Key 1") secret = key.generate_secret() key.save() - auth = '%s:%s' % (key.key, secret) - header = 'Basic %s' % base64.b64encode(auth.encode()).decode() + auth = "%s:%s" % (key.key, secret) + header = "Basic %s" % base64.b64encode(auth.encode()).decode() return AuthKey(key=key, header=header) -@pytest.mark.parametrize( - 'http_method', ['put', 'post', 'delete', 'options', 'head']) -@pytest.mark.parametrize( - 'endpoint', ['children', 'toc', 'json', 'json_slug']) +@pytest.mark.parametrize("http_method", ["put", "post", "delete", "options", "head"]) +@pytest.mark.parametrize("endpoint", ["children", "toc", "json", "json_slug"]) def test_disallowed_methods(client, http_method, endpoint): """HTTP methods other than GET & HEAD are not allowed.""" headers = {} kwargs = None - if endpoint != 'json': - kwargs = dict(document_path='Web/CSS') - if endpoint == 'toc': + if endpoint != "json": + kwargs = dict(document_path="Web/CSS") + if endpoint == "toc": headers.update(HTTP_HOST=settings.WIKI_HOST) - url = reverse('wiki.{}'.format(endpoint), kwargs=kwargs) + url = reverse("wiki.{}".format(endpoint), kwargs=kwargs) response = getattr(client, http_method)(url, **headers) assert response.status_code == 405 assert_shared_cache_header(response) -@pytest.mark.parametrize('method', ('GET', 'HEAD')) -@pytest.mark.parametrize('if_none_match', (None, 'match', 'mismatch')) +@pytest.mark.parametrize("method", ("GET", "HEAD")) +@pytest.mark.parametrize("if_none_match", (None, "match", "mismatch")) @pytest.mark.parametrize( - 'section_case', - ('no-section', 'section', 'another-section', 'non-existent-section') + "section_case", ("no-section", "section", "another-section", "non-existent-section") ) def test_api_safe(client, section_doc, section_case, if_none_match, method): """ @@ -142,53 +149,52 @@ def test_api_safe(client, section_doc, section_case, if_none_match, method): """ section_id, exp_content = SECTION_CASE_TO_DETAILS[section_case] - url = section_doc.get_absolute_url() + '$api' + url = section_doc.get_absolute_url() + "$api" if section_id: - url += '?section={}'.format(section_id) + url += "?section={}".format(section_id) headers = dict(HTTP_HOST=settings.WIKI_HOST) - if method == 'GET': + if method == "GET": # Starting with Django 1.11, condition headers will be # considered only for GET requests. The one exception # is a PUT request to the wiki.document_api endpoint, # but that's not relevant here. - if if_none_match == 'match': + if if_none_match == "match": response = getattr(client, method.lower())(url, **headers) - assert 'etag' in response - headers['HTTP_IF_NONE_MATCH'] = response['etag'] - elif if_none_match == 'mismatch': - headers['HTTP_IF_NONE_MATCH'] = 'ABC' + assert "etag" in response + headers["HTTP_IF_NONE_MATCH"] = response["etag"] + elif if_none_match == "mismatch": + headers["HTTP_IF_NONE_MATCH"] = "ABC" response = getattr(client, method.lower())(url, **headers) - if (if_none_match == 'match') and (method == 'GET'): - exp_content = '' + if (if_none_match == "match") and (method == "GET"): + exp_content = "" assert response.status_code == 304 else: assert response.status_code == 200 assert_shared_cache_header(response) - assert 'last-modified' not in response - if method == 'GET': - assert quote_etag(calculate_etag(exp_content)) in response['etag'] - assert (response['x-kuma-revision'] == - str(section_doc.current_revision_id)) + assert "last-modified" not in response + if method == "GET": + assert quote_etag(calculate_etag(exp_content)) in response["etag"] + assert response["x-kuma-revision"] == str(section_doc.current_revision_id) - if method == 'GET': + if method == "GET": assert response.content.decode(response.charset) == exp_content -@pytest.mark.parametrize('user_case', ('authenticated', 'anonymous')) -def test_api_put_forbidden_when_no_authkey(client, user_client, root_doc, - user_case): +@pytest.mark.parametrize("user_case", ("authenticated", "anonymous")) +def test_api_put_forbidden_when_no_authkey(client, user_client, root_doc, user_case): """ A PUT to the wiki.document_api endpoint should forbid access without an authkey, even for logged-in users. """ - url = root_doc.get_absolute_url() + '$api' - response = (client if user_case == 'anonymous' else user_client).put( - url, HTTP_HOST=settings.WIKI_HOST) + url = root_doc.get_absolute_url() + "$api" + response = (client if user_case == "anonymous" else user_client).put( + url, HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 403 assert_no_cache_header(response) @@ -198,13 +204,13 @@ def test_api_put_unsupported_content_type(client, authkey): A PUT to the wiki.document_api endpoint with an unsupported content type should return a 400. """ - url = '/en-US/docs/foobar$api' + url = "/en-US/docs/foobar$api" response = client.put( url, - data='stuff', - content_type='nonsense', + data="stuff", + content_type="nonsense", HTTP_AUTHORIZATION=authkey.header, - HTTP_HOST=settings.WIKI_HOST + HTTP_HOST=settings.WIKI_HOST, ) assert response.status_code == 400 assert_shared_cache_header(response) @@ -214,50 +220,44 @@ def test_api_put_authkey_tracking(client, authkey): """ Revisions modified by PUT API should track the auth key used """ - url = '/en-US/docs/foobar$api' - data = dict( - title="Foobar, The Document", - content='

    Hello, I am foobar.

    ', - ) - content_type, encoded_data = get_content('json', data) + url = "/en-US/docs/foobar$api" + data = dict(title="Foobar, The Document", content="

    Hello, I am foobar.

    ",) + content_type, encoded_data = get_content("json", data) response = client.put( url, data=encoded_data, content_type=content_type, HTTP_AUTHORIZATION=authkey.header, - HTTP_HOST=settings.WIKI_HOST + HTTP_HOST=settings.WIKI_HOST, ) assert response.status_code == 201 assert_shared_cache_header(response) - last_log = authkey.key.history.order_by('-pk').all()[0] - assert last_log.action == 'created' + last_log = authkey.key.history.order_by("-pk").all()[0] + assert last_log.action == "created" - data['title'] = 'Foobar, The New Document' - content_type, encoded_data = get_content('json', data) + data["title"] = "Foobar, The New Document" + content_type, encoded_data = get_content("json", data) response = client.put( url, data=encoded_data, content_type=content_type, HTTP_AUTHORIZATION=authkey.header, - HTTP_HOST=settings.WIKI_HOST + HTTP_HOST=settings.WIKI_HOST, ) assert response.status_code == 205 assert_shared_cache_header(response) - last_log = authkey.key.history.order_by('-pk').all()[0] - assert last_log.action == 'updated' + last_log = authkey.key.history.order_by("-pk").all()[0] + assert last_log.action == "updated" -@pytest.mark.parametrize('if_match', (None, 'match', 'mismatch')) -@pytest.mark.parametrize( - 'content_case', - ('multipart', 'json', 'html-fragment', 'html') -) +@pytest.mark.parametrize("if_match", (None, "match", "mismatch")) +@pytest.mark.parametrize("content_case", ("multipart", "json", "html-fragment", "html")) @pytest.mark.parametrize( - 'section_case', - ('no-section', 'section', 'another-section', 'non-existent-section') + "section_case", ("no-section", "section", "another-section", "non-existent-section") ) -def test_api_put_existing(settings, client, section_doc, authkey, section_case, - content_case, if_match): +def test_api_put_existing( + settings, client, section_doc, authkey, section_case, content_case, if_match +): """ A PUT to the wiki.document_api endpoint should allow the modification of an existing document's content. @@ -265,52 +265,46 @@ def test_api_put_existing(settings, client, section_doc, authkey, section_case, orig_rev_id = section_doc.current_revision_id section_id, section_content = SECTION_CASE_TO_DETAILS[section_case] - url = section_doc.get_absolute_url() + '$api' + url = section_doc.get_absolute_url() + "$api" if section_id: - url += '?section={}'.format(section_id) + url += "?section={}".format(section_id) - headers = dict(HTTP_AUTHORIZATION=authkey.header, - HTTP_HOST=settings.WIKI_HOST) + headers = dict(HTTP_AUTHORIZATION=authkey.header, HTTP_HOST=settings.WIKI_HOST) - if if_match == 'match': + if if_match == "match": response = client.get(url, HTTP_HOST=settings.WIKI_HOST) - assert 'etag' in response - headers['HTTP_IF_MATCH'] = response['etag'] - elif if_match == 'mismatch': - headers['HTTP_IF_MATCH'] = 'ABC' + assert "etag" in response + headers["HTTP_IF_MATCH"] = response["etag"] + elif if_match == "mismatch": + headers["HTTP_IF_MATCH"] = "ABC" data = dict( comment="I like this document.", title="New Sectioned Root Document", summary="An edited sectioned root document.", - content=EMPTY_IFRAME + '

    This is an edit.

    ', + content=EMPTY_IFRAME + "

    This is an edit.

    ", tags="tagA,tagB,tagC", review_tags="editorial,technical", ) content_type, encoded_data = get_content(content_case, data) - response = client.put( - url, - data=encoded_data, - content_type=content_type, - **headers - ) + response = client.put(url, data=encoded_data, content_type=content_type, **headers) - if content_case == 'html-fragment': + if content_case == "html-fragment": expected_title = section_doc.title else: - expected_title = data['title'] + expected_title = data["title"] if section_content: - expected_content = SECTIONS.replace(section_content, data['content']) + expected_content = SECTIONS.replace(section_content, data["content"]) else: expected_content = SECTIONS assert_shared_cache_header(response) - if if_match == 'mismatch': + if if_match == "mismatch": assert response.status_code == 412 else: assert response.status_code == 205 @@ -319,23 +313,20 @@ def test_api_put_existing(settings, client, section_doc, authkey, section_case, assert section_doc.current_revision_id != orig_rev_id assert section_doc.title == expected_title assert section_doc.html == expected_content - if content_case in ('multipart', 'json'): + if content_case in ("multipart", "json"): rev = section_doc.current_revision - assert rev.summary == data['summary'] - assert rev.comment == data['comment'] - assert rev.tags == data['tags'] - assert (set(rev.review_tags.names()) == - set(data['review_tags'].split(','))) - - -@pytest.mark.parametrize('slug_case', ('root', 'child', 'nonexistent-parent')) -@pytest.mark.parametrize( - 'content_case', - ('multipart', 'json', 'html-fragment', 'html') -) -@pytest.mark.parametrize('section_case', ('no-section', 'section')) -def test_api_put_new(settings, client, root_doc, authkey, section_case, - content_case, slug_case): + assert rev.summary == data["summary"] + assert rev.comment == data["comment"] + assert rev.tags == data["tags"] + assert set(rev.review_tags.names()) == set(data["review_tags"].split(",")) + + +@pytest.mark.parametrize("slug_case", ("root", "child", "nonexistent-parent")) +@pytest.mark.parametrize("content_case", ("multipart", "json", "html-fragment", "html")) +@pytest.mark.parametrize("section_case", ("no-section", "section")) +def test_api_put_new( + settings, client, root_doc, authkey, section_case, content_case, slug_case +): """ A PUT to the wiki.document_api endpoint should allow the creation of a new document and its initial revision. @@ -343,19 +334,19 @@ def test_api_put_new(settings, client, root_doc, authkey, section_case, locale = settings.WIKI_DEFAULT_LANGUAGE section_id, _ = SECTION_CASE_TO_DETAILS[section_case] - if slug_case == 'root': - slug = 'foobar' - elif slug_case == 'child': - slug = 'Root/foobar' + if slug_case == "root": + slug = "foobar" + elif slug_case == "child": + slug = "Root/foobar" else: - slug = 'nonexistent/foobar' + slug = "nonexistent/foobar" - url_path = '/{}/docs/{}'.format(locale, slug) - url = url_path + '$api' + url_path = "/{}/docs/{}".format(locale, slug) + url = url_path + "$api" # The section_id should have no effect on the results, but we'll see. if section_id: - url += '?section={}'.format(section_id) + url += "?section={}".format(section_id) data = dict( comment="I like this document.", @@ -373,41 +364,40 @@ def test_api_put_new(settings, client, root_doc, authkey, section_case, data=encoded_data, content_type=content_type, HTTP_AUTHORIZATION=authkey.header, - HTTP_HOST=settings.WIKI_HOST + HTTP_HOST=settings.WIKI_HOST, ) - if content_case == 'html-fragment': + if content_case == "html-fragment": expected_title = slug else: - expected_title = data['title'] + expected_title = data["title"] - if slug_case == 'nonexistent-parent': + if slug_case == "nonexistent-parent": assert response.status_code == 404 assert_no_cache_header(response) else: assert response.status_code == 201 assert_shared_cache_header(response) - assert 'location' in response - assert urlparse(response['location']).path == url_path + assert "location" in response + assert urlparse(response["location"]).path == url_path # Confirm that the PUT worked. doc = Document.objects.get(locale=locale, slug=slug) assert doc.title == expected_title - assert doc.html == data['content'] - if content_case in ('multipart', 'json'): + assert doc.html == data["content"] + if content_case in ("multipart", "json"): rev = doc.current_revision - assert rev.summary == data['summary'] - assert rev.comment == data['comment'] - assert rev.tags == data['tags'] - assert (set(rev.review_tags.names()) == - set(data['review_tags'].split(','))) + assert rev.summary == data["summary"] + assert rev.comment == data["comment"] + assert rev.tags == data["tags"] + assert set(rev.review_tags.names()) == set(data["review_tags"].split(",")) def test_apply_content_experiment_no_experiment(ce_settings, rf): """If not under a content experiment, use the original Document.""" - doc = mock.Mock(spec_set=['locale', 'slug']) - doc.locale = 'en-US' - doc.slug = 'Other' - request = rf.get('/%s/docs/%s' % (doc.locale, doc.slug)) + doc = mock.Mock(spec_set=["locale", "slug"]) + doc.locale = "en-US" + doc.slug = "Other" + request = rf.get("/%s/docs/%s" % (doc.locale, doc.slug)) experiment_doc, params = _apply_content_experiment(request, doc) @@ -417,113 +407,111 @@ def test_apply_content_experiment_no_experiment(ce_settings, rf): def test_apply_content_experiment_has_experiment(ce_settings, rf): """If under a content experiment, return original Document and params.""" - doc = mock.Mock(spec_set=['locale', 'slug']) - doc.locale = 'en-US' - doc.slug = 'Original' - request = rf.get('/%s/docs/%s' % (doc.locale, doc.slug)) + doc = mock.Mock(spec_set=["locale", "slug"]) + doc.locale = "en-US" + doc.slug = "Original" + request = rf.get("/%s/docs/%s" % (doc.locale, doc.slug)) experiment_doc, params = _apply_content_experiment(request, doc) assert experiment_doc == doc assert params == { - 'id': 'experiment-test', - 'ga_name': 'experiment-test', - 'param': 'v', - 'original_path': '/en-US/docs/Original', - 'variants': { - 'control': 'Original', - 'test': 'Experiment:Test/Variant', - }, - 'selected': None, - 'selection_is_valid': None, + "id": "experiment-test", + "ga_name": "experiment-test", + "param": "v", + "original_path": "/en-US/docs/Original", + "variants": {"control": "Original", "test": "Experiment:Test/Variant"}, + "selected": None, + "selection_is_valid": None, } def test_apply_content_experiment_selected_original(ce_settings, rf): """If the original is selected as the content experiment, return it.""" - doc = mock.Mock(spec_set=['locale', 'slug']) - db_doc = mock.Mock(spec_set=['locale', 'slug']) - doc.locale = db_doc.locale = 'en-US' - doc.slug = db_doc.slug = 'Original' - request = rf.get('/%s/docs/%s' % (doc.locale, doc.slug), {'v': 'control'}) + doc = mock.Mock(spec_set=["locale", "slug"]) + db_doc = mock.Mock(spec_set=["locale", "slug"]) + doc.locale = db_doc.locale = "en-US" + doc.slug = db_doc.slug = "Original" + request = rf.get("/%s/docs/%s" % (doc.locale, doc.slug), {"v": "control"}) with mock.patch( - 'kuma.wiki.views.document.Document.objects.get', - return_value=db_doc) as mock_get: + "kuma.wiki.views.document.Document.objects.get", return_value=db_doc + ) as mock_get: experiment_doc, params = _apply_content_experiment(request, doc) - mock_get.assert_called_once_with(locale='en-US', slug='Original') + mock_get.assert_called_once_with(locale="en-US", slug="Original") assert experiment_doc == db_doc - assert params['selected'] == 'control' - assert params['selection_is_valid'] + assert params["selected"] == "control" + assert params["selection_is_valid"] def test_apply_content_experiment_selected_variant(ce_settings, rf): """If the variant is selected as the content experiment, return it.""" - doc = mock.Mock(spec_set=['locale', 'slug']) - db_doc = mock.Mock(spec_set=['locale', 'slug']) - doc.locale = db_doc.locale = 'en-US' - doc.slug = 'Original' - db_doc.slug = 'Experiment:Test/Variant' - request = rf.get('/%s/docs/%s' % (doc.locale, doc.slug), {'v': 'test'}) + doc = mock.Mock(spec_set=["locale", "slug"]) + db_doc = mock.Mock(spec_set=["locale", "slug"]) + doc.locale = db_doc.locale = "en-US" + doc.slug = "Original" + db_doc.slug = "Experiment:Test/Variant" + request = rf.get("/%s/docs/%s" % (doc.locale, doc.slug), {"v": "test"}) with mock.patch( - 'kuma.wiki.views.document.Document.objects.get', - return_value=db_doc) as mock_get: + "kuma.wiki.views.document.Document.objects.get", return_value=db_doc + ) as mock_get: experiment_doc, params = _apply_content_experiment(request, doc) - mock_get.assert_called_once_with(locale='en-US', - slug='Experiment:Test/Variant') + mock_get.assert_called_once_with(locale="en-US", slug="Experiment:Test/Variant") assert experiment_doc == db_doc - assert params['selected'] == 'test' - assert params['selection_is_valid'] + assert params["selected"] == "test" + assert params["selection_is_valid"] def test_apply_content_experiment_bad_selection(ce_settings, rf): """If the variant is selected as the content experiment, return it.""" - doc = mock.Mock(spec_set=['locale', 'slug']) - doc.locale = 'en-US' - doc.slug = 'Original' - request = rf.get('/%s/docs/%s' % (doc.locale, doc.slug), {'v': 'other'}) + doc = mock.Mock(spec_set=["locale", "slug"]) + doc.locale = "en-US" + doc.slug = "Original" + request = rf.get("/%s/docs/%s" % (doc.locale, doc.slug), {"v": "other"}) experiment_doc, params = _apply_content_experiment(request, doc) assert experiment_doc == doc - assert params['selected'] is None - assert not params['selection_is_valid'] + assert params["selected"] is None + assert not params["selection_is_valid"] def test_apply_content_experiment_valid_selection_no_doc(ce_settings, rf): """If the Document for a variant doesn't exist, return the original.""" - doc = mock.Mock(spec_set=['locale', 'slug']) - doc.locale = 'en-US' - doc.slug = 'Original' - request = rf.get('/%s/docs/%s' % (doc.locale, doc.slug), {'v': 'test'}) + doc = mock.Mock(spec_set=["locale", "slug"]) + doc.locale = "en-US" + doc.slug = "Original" + request = rf.get("/%s/docs/%s" % (doc.locale, doc.slug), {"v": "test"}) with mock.patch( - 'kuma.wiki.views.document.Document.objects.get', - side_effect=Document.DoesNotExist) as mock_get: + "kuma.wiki.views.document.Document.objects.get", + side_effect=Document.DoesNotExist, + ) as mock_get: experiment_doc, params = _apply_content_experiment(request, doc) - mock_get.assert_called_once_with(locale='en-US', - slug='Experiment:Test/Variant') + mock_get.assert_called_once_with(locale="en-US", slug="Experiment:Test/Variant") assert experiment_doc == doc - assert params['selected'] is None - assert not params['selection_is_valid'] + assert params["selected"] is None + assert not params["selection_is_valid"] def test_document_banned_ip_can_read(client, root_doc): - '''Banned IPs are still allowed to read content, just not edit.''' - ip = '127.0.0.1' + """Banned IPs are still allowed to read content, just not edit.""" + ip = "127.0.0.1" IPBan.objects.create(ip=ip) - response = client.get(root_doc.get_absolute_url(), REMOTE_ADDR=ip, - HTTP_HOST=settings.WIKI_HOST) + response = client.get( + root_doc.get_absolute_url(), REMOTE_ADDR=ip, HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 200 -@pytest.mark.parametrize('endpoint', ['document', 'preview']) -def test_kumascript_error_reporting(admin_client, root_doc, ks_toolbox, - endpoint, mock_requests): +@pytest.mark.parametrize("endpoint", ["document", "preview"]) +def test_kumascript_error_reporting( + admin_client, root_doc, ks_toolbox, endpoint, mock_requests +): """ Kumascript reports errors in HTTP headers. Kuma should display the errors with appropriate links for both the "wiki.preview" and "wiki.document" @@ -533,58 +521,56 @@ def test_kumascript_error_reporting(admin_client, root_doc, ks_toolbox, KUMASCRIPT_TIMEOUT=1.0, KUMASCRIPT_MAX_AGE=600, KUMA_DOCUMENT_FORCE_DEFERRED_TIMEOUT=10.0, - KUMA_DOCUMENT_RENDER_TIMEOUT=180.0 + KUMA_DOCUMENT_RENDER_TIMEOUT=180.0, ) - mock_ks_config = mock.patch('kuma.wiki.kumascript.config', **ks_settings) + mock_ks_config = mock.patch("kuma.wiki.kumascript.config", **ks_settings) with mock_ks_config: mock_requests.post( - requests_mock.ANY, - text='HELLO WORLD', - headers=ks_toolbox.errors_as_headers, - ) - mock_requests.get( - requests_mock.ANY, - **ks_toolbox.macros_response + requests_mock.ANY, text="HELLO WORLD", headers=ks_toolbox.errors_as_headers, ) - if endpoint == 'preview': + mock_requests.get(requests_mock.ANY, **ks_toolbox.macros_response) + if endpoint == "preview": response = admin_client.post( - reverse('wiki.preview'), - dict(content=b'anything truthy'), - HTTP_HOST=settings.WIKI_HOST + reverse("wiki.preview"), + dict(content=b"anything truthy"), + HTTP_HOST=settings.WIKI_HOST, ) else: - with mock.patch('kuma.wiki.models.config', **ks_settings): - response = admin_client.get(root_doc.get_absolute_url(), - HTTP_HOST=settings.WIKI_HOST) + with mock.patch("kuma.wiki.models.config", **ks_settings): + response = admin_client.get( + root_doc.get_absolute_url(), HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 200 content = response.content.decode(response.charset) response_html = pq(content) - macro_link = ('#kserrors-list a[href="https://github.com/' - 'mdn/kumascript/blob/master/macros/{}.ejs"]') - create_link = ('#kserrors-list a[href="https://github.com/' - 'mdn/kumascript#updating-macros"]') - assert len(response_html.find(macro_link.format('SomeMacro'))) == 1 + macro_link = ( + '#kserrors-list a[href="https://github.com/' + 'mdn/kumascript/blob/master/macros/{}.ejs"]' + ) + create_link = ( + '#kserrors-list a[href="https://github.com/' 'mdn/kumascript#updating-macros"]' + ) + assert len(response_html.find(macro_link.format("SomeMacro"))) == 1 assert len(response_html.find(create_link)) == 1 - assert mock_requests.request_history[0].headers['X-FireLogger'] == '1.2' - for error in ks_toolbox.errors['logs']: - assert error['message'] in content + assert mock_requests.request_history[0].headers["X-FireLogger"] == "1.2" + for error in ks_toolbox.errors["logs"]: + assert error["message"] in content @pytest.mark.tags def test_tags_show_in_document(root_doc, client, wiki_user): """Test tags are showing correctly in document view""" - tags = ('JavaScript', 'AJAX', 'DOM') - Revision.objects.create(document=root_doc, tags=','.join(tags), creator=wiki_user) - response = client.get(root_doc.get_absolute_url(), - HTTP_HOST=settings.WIKI_HOST) + tags = ("JavaScript", "AJAX", "DOM") + Revision.objects.create(document=root_doc, tags=",".join(tags), creator=wiki_user) + response = client.get(root_doc.get_absolute_url(), HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 page = pq(response.content) - response_tags = page.find('.tags li a').contents() + response_tags = page.find(".tags li a").contents() assert len(response_tags) == len(tags) # The response tags should be sorted assert response_tags == sorted(tags) @@ -593,28 +579,28 @@ def test_tags_show_in_document(root_doc, client, wiki_user): @pytest.mark.tags def test_tags_not_show_while_empty(root_doc, client, wiki_user): # Create a revision with no tags - Revision.objects.create(document=root_doc, tags=','.join([]), creator=wiki_user) + Revision.objects.create(document=root_doc, tags=",".join([]), creator=wiki_user) - response = client.get(root_doc.get_absolute_url(), - HTTP_HOST=settings.WIKI_HOST) + response = client.get(root_doc.get_absolute_url(), HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 page = pq(response.content) - response_tags = page.find('.tags li a').contents() + response_tags = page.find(".tags li a").contents() # There should be no tag assert len(response_tags) == 0 @pytest.mark.parametrize( - 'params_case', - ['nothing', 'title-only', 'slug-only', 'title-and-slug', 'missing-title']) + "params_case", + ["nothing", "title-only", "slug-only", "title-and-slug", "missing-title"], +) def test_json(doc_hierarchy, client, params_case): """Test the wiki.json endpoint.""" top_doc = doc_hierarchy.top bottom_doc = doc_hierarchy.bottom - expected_tags = sorted(['foo', 'bar', 'baz']) - expected_review_tags = sorted(['tech', 'editorial']) + expected_tags = sorted(["foo", "bar", "baz"]) + expected_review_tags = sorted(["tech", "editorial"]) for doc in (top_doc, bottom_doc): doc.tags.set(*expected_tags) @@ -623,23 +609,23 @@ def test_json(doc_hierarchy, client, params_case): params = None expected_slug = None expected_status_code = 200 - if params_case == 'nothing': + if params_case == "nothing": expected_status_code = 400 - elif params_case == 'title-only': + elif params_case == "title-only": expected_slug = top_doc.slug params = dict(title=top_doc.title) - elif params_case == 'slug-only': + elif params_case == "slug-only": expected_slug = bottom_doc.slug params = dict(slug=bottom_doc.slug) - elif params_case == 'title-and-slug': + elif params_case == "title-and-slug": expected_slug = top_doc.slug params = dict(title=top_doc.title, slug=bottom_doc.slug) else: # missing title expected_status_code = 404 - params = dict(title='nonexistent document title') + params = dict(title="nonexistent document title") - url = reverse('wiki.json') - with override_switch('application_ACAO', True): + url = reverse("wiki.json") + with override_switch("application_ACAO", True): response = client.get(url, params) assert response.status_code == expected_status_code @@ -647,41 +633,39 @@ def test_json(doc_hierarchy, client, params_case): assert_no_cache_header(response) else: assert_shared_cache_header(response) - assert response['Access-Control-Allow-Origin'] == '*' + assert response["Access-Control-Allow-Origin"] == "*" if response.status_code == 200: data = json.loads(response.content) - assert data['slug'] == expected_slug - assert sorted(data['tags']) == expected_tags - assert sorted(data['review_tags']) == expected_review_tags + assert data["slug"] == expected_slug + assert sorted(data["tags"]) == expected_tags + assert sorted(data["review_tags"]) == expected_review_tags -@pytest.mark.parametrize('params_case', ['with-params', 'without-params']) +@pytest.mark.parametrize("params_case", ["with-params", "without-params"]) def test_fallback_to_translation(root_doc, trans_doc, client, params_case): """ If a slug isn't found in the requested locale but is in the default locale and if there is a translation of that default-locale document to the requested locale, the translation should be served. """ - params = '?x=y&x=z' if (params_case == 'with-params') else '' - url = reverse('wiki.document', args=[root_doc.slug], locale='fr') + params = "?x=y&x=z" if (params_case == "with-params") else "" + url = reverse("wiki.document", args=[root_doc.slug], locale="fr") response = client.get(url + params, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 302 assert_shared_cache_header(response) - assert response['Location'].endswith(trans_doc.get_absolute_url() + params) + assert response["Location"].endswith(trans_doc.get_absolute_url() + params) def test_redirect_with_no_slug(db, client): """Bug 775241: Fix exception in redirect for URL with ui-locale""" - url = '/en-US/docs/en-US/' + url = "/en-US/docs/en-US/" response = client.get(url) assert response.status_code == 404 assert_no_cache_header(response) -@pytest.mark.parametrize( - 'http_method', ['get', 'put', 'delete', 'options', 'head']) -@pytest.mark.parametrize( - 'endpoint', ['wiki.subscribe', 'wiki.subscribe_to_tree']) +@pytest.mark.parametrize("http_method", ["get", "put", "delete", "options", "head"]) +@pytest.mark.parametrize("endpoint", ["wiki.subscribe", "wiki.subscribe_to_tree"]) def test_watch_405(client, root_doc, endpoint, http_method): """Watch document with HTTP non-POST request results in 405.""" url = reverse(endpoint, args=[root_doc.slug]) @@ -690,22 +674,26 @@ def test_watch_405(client, root_doc, endpoint, http_method): assert_no_cache_header(response) -@pytest.mark.parametrize( - 'endpoint', ['wiki.subscribe', 'wiki.subscribe_to_tree']) +@pytest.mark.parametrize("endpoint", ["wiki.subscribe", "wiki.subscribe_to_tree"]) def test_watch_login_required(client, root_doc, endpoint): """User must be logged-in to subscribe to a document.""" url = reverse(endpoint, args=[root_doc.slug]) response = client.post(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 302 assert_no_cache_header(response) - assert response['Location'].endswith( - reverse('account_login') + '?next=' + quote(url)) + assert response["Location"].endswith( + reverse("account_login") + "?next=" + quote(url) + ) @pytest.mark.parametrize( - 'endpoint,event', [('wiki.subscribe', EditDocumentEvent), - ('wiki.subscribe_to_tree', EditDocumentInTreeEvent)], - ids=['subscribe', 'subscribe_to_tree']) + "endpoint,event", + [ + ("wiki.subscribe", EditDocumentEvent), + ("wiki.subscribe_to_tree", EditDocumentInTreeEvent), + ], + ids=["subscribe", "subscribe_to_tree"], +) def test_watch_unwatch(user_client, wiki_user, root_doc, endpoint, event): """Watch and unwatch a document.""" url = reverse(endpoint, args=[root_doc.slug]) @@ -713,179 +701,195 @@ def test_watch_unwatch(user_client, wiki_user, root_doc, endpoint, event): response = user_client.post(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 302 assert_no_cache_header(response) - assert response['Location'].endswith( - reverse('wiki.document', args=[root_doc.slug])) - assert event.is_notifying(wiki_user, root_doc), 'Watch was not created' + assert response["Location"].endswith(reverse("wiki.document", args=[root_doc.slug])) + assert event.is_notifying(wiki_user, root_doc), "Watch was not created" # Unsubscribe response = user_client.post(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 302 assert_no_cache_header(response) - assert response['Location'].endswith( - reverse('wiki.document', args=[root_doc.slug])) - assert not event.is_notifying(wiki_user, root_doc), \ - 'Watch was not destroyed' + assert response["Location"].endswith(reverse("wiki.document", args=[root_doc.slug])) + assert not event.is_notifying(wiki_user, root_doc), "Watch was not destroyed" def test_deleted_doc_anon(deleted_doc, client): """Requesting a deleted doc returns 404""" - response = client.get(deleted_doc.get_absolute_url(), - HTTP_HOST=settings.WIKI_HOST) + response = client.get(deleted_doc.get_absolute_url(), HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 404 content = response.content.decode(response.charset) assert "This document was deleted" not in content - assert 'Reason for Deletion' not in content + assert "Reason for Deletion" not in content def test_deleted_doc_user(deleted_doc, user_client): """Requesting a deleted doc returns 404, deletion message""" - response = user_client.get(deleted_doc.get_absolute_url(), - HTTP_HOST=settings.WIKI_HOST) + response = user_client.get( + deleted_doc.get_absolute_url(), HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 404 content = response.content.decode(response.charset) assert "This document was deleted" not in content - assert 'Reason for Deletion' not in content - assert 'Restore this document' not in content - assert 'Purge this document' not in content + assert "Reason for Deletion" not in content + assert "Restore this document" not in content + assert "Purge this document" not in content def test_deleted_doc_moderator(deleted_doc, moderator_client): """Requesting deleted doc as moderator returns 404 with action buttons.""" - response = moderator_client.get(deleted_doc.get_absolute_url(), - HTTP_HOST=settings.WIKI_HOST) + response = moderator_client.get( + deleted_doc.get_absolute_url(), HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 404 content = response.content.decode(response.charset) - assert 'Reason for Deletion' in content + assert "Reason for Deletion" in content full_description = ( - 'This document was deleted by' + "This document was deleted by" ' moderator' ' on .') + "August 21, 2018 at 5:22:00 PM PDT." + ) assert full_description in content - assert 'Restore this document' in content - assert 'Purge this document' in content + assert "Restore this document" in content + assert "Purge this document" in content -def test_deleted_doc_no_purge_permdeleted(deleted_doc, wiki_moderator, - moderator_client): +def test_deleted_doc_no_purge_permdeleted( + deleted_doc, wiki_moderator, moderator_client +): """Requesting deleted doc without purge perm removes purge button.""" wiki_moderator.user_permissions.remove( - Permission.objects.get(codename='purge_document')) - response = moderator_client.get(deleted_doc.get_absolute_url(), - HTTP_HOST=settings.WIKI_HOST) + Permission.objects.get(codename="purge_document") + ) + response = moderator_client.get( + deleted_doc.get_absolute_url(), HTTP_HOST=settings.WIKI_HOST + ) assert response.status_code == 404 content = response.content.decode(response.charset) - assert 'Reason for Deletion' in content + assert "Reason for Deletion" in content full_description = ( - 'This document was deleted by' + "This document was deleted by" ' moderator' ' on .') + "August 21, 2018 at 5:22:00 PM PDT." + ) assert full_description in content - assert 'Restore this document' in content - assert 'Purge this document' not in content + assert "Restore this document" in content + assert "Purge this document" not in content -@pytest.mark.parametrize('case', ('DOMAIN', 'WIKI_HOST')) +@pytest.mark.parametrize("case", ("DOMAIN", "WIKI_HOST")) def test_redirect_suppression(client, settings, root_doc, redirect_doc, case): """The document view shouldn't redirect when passed redirect=no.""" host = getattr(settings, case) url = redirect_doc.get_absolute_url() response = client.get(url, HTTP_HOST=host) assert response.status_code == 301 - response = client.get(url + '?redirect=no', HTTP_HOST=host) + response = client.get(url + "?redirect=no", HTTP_HOST=host) assert response.status_code == 200 -@pytest.mark.parametrize( - 'href', ['//davidwalsh.name', 'http://davidwalsh.name']) -@mock.patch('kuma.wiki.kumascript.get') -def test_redirects_only_internal(mock_kumascript_get, constance_config, - wiki_user, client, href): +@pytest.mark.parametrize("href", ["//davidwalsh.name", "http://davidwalsh.name"]) +@mock.patch("kuma.wiki.kumascript.get") +def test_redirects_only_internal( + mock_kumascript_get, constance_config, wiki_user, client, href +): """Ensures redirects cannot be used to link to other sites""" constance_config.KUMASCRIPT_TIMEOUT = 1 redirect_doc = Document.objects.create( - locale='en-US', slug='Redirection', title='External Redirect Document') + locale="en-US", slug="Redirection", title="External Redirect Document" + ) Revision.objects.create( document=redirect_doc, creator=wiki_user, - content=REDIRECT_CONTENT % {'href': href, 'title': 'DWB'}, - title='External Redirect Document', - created=datetime(2018, 4, 18, 12, 15)) + content=REDIRECT_CONTENT % {"href": href, "title": "DWB"}, + title="External Redirect Document", + created=datetime(2018, 4, 18, 12, 15), + ) mock_kumascript_get.return_value = (redirect_doc.html, None) url = redirect_doc.get_absolute_url() response = client.get(url, follow=True, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 assert not response.redirect_chain content = response.content.decode(response.charset) - body = pq(content).find('#wikiArticle') - assert body.text() == 'REDIRECT DWB' + body = pq(content).find("#wikiArticle") + assert body.text() == "REDIRECT DWB" assert body.find('a[href="{}"]'.format(href)) -@mock.patch('kuma.wiki.kumascript.get') -def test_self_redirect_supression(mock_kumascript_get, constance_config, - wiki_user, client): +@mock.patch("kuma.wiki.kumascript.get") +def test_self_redirect_supression( + mock_kumascript_get, constance_config, wiki_user, client +): """The document view shouldn't redirect to itself.""" constance_config.KUMASCRIPT_TIMEOUT = 1 redirect_doc = Document.objects.create( - locale='en-US', slug='Redirection', title='Self Redirect Document') + locale="en-US", slug="Redirection", title="Self Redirect Document" + ) url = redirect_doc.get_absolute_url() Revision.objects.create( document=redirect_doc, creator=wiki_user, - content=REDIRECT_CONTENT % {'href': url, 'title': 'Self Redirection'}, - title='Self Redirect Document', - created=datetime(2018, 4, 19, 12, 15)) + content=REDIRECT_CONTENT % {"href": url, "title": "Self Redirection"}, + title="Self Redirect Document", + created=datetime(2018, 4, 19, 12, 15), + ) mock_kumascript_get.return_value = (redirect_doc.html, None) response = client.get(url, follow=True, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 assert not response.redirect_chain content = response.content.decode(response.charset) - body = pq(content).find('#wikiArticle') - assert body.text() == 'REDIRECT Self Redirection' + body = pq(content).find("#wikiArticle") + assert body.text() == "REDIRECT Self Redirection" assert body.find('a[href="{}"][class="redirect"]'.format(url)) -@pytest.mark.parametrize('locales,expected_results', - HREFLANG_TEST_CASES.values(), - ids=tuple(HREFLANG_TEST_CASES.keys())) +@pytest.mark.parametrize( + "locales,expected_results", + HREFLANG_TEST_CASES.values(), + ids=tuple(HREFLANG_TEST_CASES.keys()), +) def test_hreflang(client, root_doc, locales, expected_results): docs = [ Document.objects.create( locale=locale, - slug='Root', - title='Root Document', - rendered_html='

    ...

    ', - parent=root_doc - ) for locale in locales + slug="Root", + title="Root Document", + rendered_html="

    ...

    ", + parent=root_doc, + ) + for locale in locales ] for doc, expected_result in zip(docs, expected_results): url = doc.get_absolute_url() response = client.get(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200, url html = pq(response.content.decode(response.charset)) - assert html.attr('lang') == expected_result - assert html.find('head > link[hreflang="{}"][href$="{}"]'.format( - expected_result, url)) + assert html.attr("lang") == expected_result + assert html.find( + 'head > link[hreflang="{}"][href$="{}"]'.format(expected_result, url) + ) @pytest.mark.parametrize( - 'param,status', - (('utm_source=docs.com', 200), - ('redirect=no', 200), - ('nocreate=1', 200), - ('edit_links=1', 301), - ('include=1', 301), - ('macros=1', 301), - ('nomacros=1', 301), - ('raw=1', 301), - ('section=junk', 301), - ('summary=1', 301))) -@mock.patch('kuma.wiki.kumascript.get') -@mock.patch('kuma.wiki.templatetags.ssr.server_side_render') -def test_wiki_only_query_params(mock_ssr, mock_kumascript_get, constance_config, - client, root_doc, param, status): + "param,status", + ( + ("utm_source=docs.com", 200), + ("redirect=no", 200), + ("nocreate=1", 200), + ("edit_links=1", 301), + ("include=1", 301), + ("macros=1", 301), + ("nomacros=1", 301), + ("raw=1", 301), + ("section=junk", 301), + ("summary=1", 301), + ), +) +@mock.patch("kuma.wiki.kumascript.get") +@mock.patch("kuma.wiki.templatetags.ssr.server_side_render") +def test_wiki_only_query_params( + mock_ssr, mock_kumascript_get, constance_config, client, root_doc, param, status +): """ The document view should ensure the wiki domain when using specific query parameters. @@ -893,9 +897,9 @@ def test_wiki_only_query_params(mock_ssr, mock_kumascript_get, constance_config, constance_config.KUMASCRIPT_TIMEOUT = 1 # For the purpose of this test, we don't care about the content of the # document page, so let's explicitly mock the "server_side_render" call. - mock_ssr.return_value = '
    ' + mock_ssr.return_value = "
    " mock_kumascript_get.return_value = (root_doc.html, None) - url = root_doc.get_absolute_url() + '?{}'.format(param) + url = root_doc.get_absolute_url() + "?{}".format(param) response = client.get(url) assert response.status_code == status if status == 301: diff --git a/kuma/wiki/tests/test_views_edit.py b/kuma/wiki/tests/test_views_edit.py index aa12ea46bf6..fd876fd1a15 100644 --- a/kuma/wiki/tests/test_views_edit.py +++ b/kuma/wiki/tests/test_views_edit.py @@ -1,4 +1,4 @@ -'''Tests for kuma/wiki/views/edit.py''' +"""Tests for kuma/wiki/views/edit.py""" import pytest @@ -10,20 +10,20 @@ def test_edit_get(editor_client, root_doc): - url = reverse('wiki.edit', args=[root_doc.slug]) + url = reverse("wiki.edit", args=[root_doc.slug]) response = editor_client.get(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 - assert response['X-Robots-Tag'] == 'noindex' + assert response["X-Robots-Tag"] == "noindex" assert_no_cache_header(response) -@pytest.mark.parametrize('method', ('GET', 'POST')) +@pytest.mark.parametrize("method", ("GET", "POST")) def test_edit_banned_ip_not_allowed(method, editor_client, root_doc): - ip = '127.0.0.1' + ip = "127.0.0.1" IPBan.objects.create(ip=ip) - url = reverse('wiki.edit', args=[root_doc.slug]) + url = reverse("wiki.edit", args=[root_doc.slug]) caller = getattr(editor_client, method.lower()) response = caller(url, REMOTE_ADDR=ip, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 403 assert_no_cache_header(response) - assert b'Your IP address has been banned.' in response.content + assert b"Your IP address has been banned." in response.content diff --git a/kuma/wiki/tests/test_views_list.py b/kuma/wiki/tests/test_views_list.py index e035521aa1d..8f15c702008 100644 --- a/kuma/wiki/tests/test_views_list.py +++ b/kuma/wiki/tests/test_views_list.py @@ -1,5 +1,3 @@ - - import pytest from django.conf import settings from pyquery import PyQuery as pq @@ -11,19 +9,28 @@ from ..models import Document +@pytest.mark.parametrize("http_method", ["put", "post", "delete", "options", "head"]) @pytest.mark.parametrize( - 'http_method', ['put', 'post', 'delete', 'options', 'head']) -@pytest.mark.parametrize( - 'endpoint', - ['tag', 'list_tags', 'all_documents', 'errors', - 'without_parent', 'top_level', 'list_review_tag', 'list_review', - 'list_with_localization_tag', 'list_with_localization_tags']) + "endpoint", + [ + "tag", + "list_tags", + "all_documents", + "errors", + "without_parent", + "top_level", + "list_review_tag", + "list_review", + "list_with_localization_tag", + "list_with_localization_tags", + ], +) def test_disallowed_methods(db, client, http_method, endpoint): """HTTP methods other than GET & HEAD are not allowed.""" kwargs = None - if endpoint in ('tag', 'list_review_tag', 'list_with_localization_tag'): - kwargs = dict(tag='tag') - url = reverse('wiki.{}'.format(endpoint), kwargs=kwargs) + if endpoint in ("tag", "list_review_tag", "list_with_localization_tag"): + kwargs = dict(tag="tag") + url = reverse("wiki.{}".format(endpoint), kwargs=kwargs) resp = getattr(client, http_method)(url, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 405 assert_shared_cache_header(resp) @@ -31,7 +38,7 @@ def test_disallowed_methods(db, client, http_method, endpoint): def test_revisions(root_doc, client): """$history of English doc works.""" - url = reverse('wiki.document_revisions', args=(root_doc.slug,)) + url = reverse("wiki.document_revisions", args=(root_doc.slug,)) resp = client.get(url, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 200 assert_shared_cache_header(resp) @@ -44,14 +51,15 @@ def test_revisions_of_translated_document(trans_doc, client): This is the revision the first translation was based on. """ assert trans_doc.revisions.count() == 1 - url = reverse('wiki.document_revisions', args=(trans_doc.slug,), - locale=trans_doc.locale) + url = reverse( + "wiki.document_revisions", args=(trans_doc.slug,), locale=trans_doc.locale + ) resp = client.get(url, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 200 page = pq(resp.content) - list_content = page('.revision-list-contain').find('li') + list_content = page(".revision-list-contain").find("li") assert len(list_content) == 2 # The translation plus the English revision - eng_rev_id = list_content.find('input[name=from]')[1].attrib['value'] + eng_rev_id = list_content.find("input[name=from]")[1].attrib["value"] assert str(trans_doc.current_revision.based_on_id) == eng_rev_id @@ -66,34 +74,35 @@ def test_revisions_of_translated_doc_with_no_based_on(trans_revision, client): trans_revision.based_on = None trans_revision.save() trans_doc = trans_revision.document - url = reverse('wiki.document_revisions', args=(trans_doc.slug,), - locale=trans_doc.locale) + url = reverse( + "wiki.document_revisions", args=(trans_doc.slug,), locale=trans_doc.locale + ) resp = client.get(url, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 200 page = pq(resp.content) - list_content = page('.revision-list-contain').find('li') + list_content = page(".revision-list-contain").find("li") assert len(list_content) == 1 # The translation alone def test_revisions_bad_slug_is_not_found(db, client): """$history of unknown page returns 404.""" - url = reverse('wiki.document_revisions', args=('not_found',)) + url = reverse("wiki.document_revisions", args=("not_found",)) resp = client.get(url, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 404 def test_revisions_doc_without_revisions_is_not_found(db, client): """$history of half-created document returns 404.""" - doc = Document.objects.create(locale='en-US', slug='half_created') - url = reverse('wiki.document_revisions', args=(doc.slug,)) + doc = Document.objects.create(locale="en-US", slug="half_created") + url = reverse("wiki.document_revisions", args=(doc.slug,)) resp = client.get(url, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 404 def test_revisions_all_params_as_anon_user_is_forbidden(root_doc, client): """Anonymous users are forbidden to request all revisions.""" - url = reverse('wiki.document_revisions', args=(root_doc.slug,)) - all_url = urlparams(url, limit='all') + url = reverse("wiki.document_revisions", args=(root_doc.slug,)) + all_url = urlparams(url, limit="all") resp = client.get(all_url, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 403 assert_shared_cache_header(resp) @@ -101,11 +110,11 @@ def test_revisions_all_params_as_anon_user_is_forbidden(root_doc, client): def test_revisions_all_params_as_user_is_allowed(root_doc, wiki_user, client): """Users are allowed to request all revisions.""" - url = reverse('wiki.document_revisions', args=(root_doc.slug,)) - all_url = urlparams(url, limit='all') - wiki_user.set_password('password') + url = reverse("wiki.document_revisions", args=(root_doc.slug,)) + all_url = urlparams(url, limit="all") + wiki_user.set_password("password") wiki_user.save() - client.login(username=wiki_user.username, password='password') + client.login(username=wiki_user.username, password="password") resp = client.get(all_url, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 200 @@ -114,216 +123,216 @@ def test_revisions_request_tiny_pages(edit_revision, client): """$history will paginate the revisions.""" doc = edit_revision.document assert doc.revisions.count() > 1 - url = reverse('wiki.document_revisions', args=(doc.slug,)) + url = reverse("wiki.document_revisions", args=(doc.slug,)) limit_url = urlparams(url, limit=1) resp = client.get(limit_url, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 200 page = pq(resp.content) - assert len(page.find('ol.pagination')) == 1 + assert len(page.find("ol.pagination")) == 1 def test_revisions_request_large_pages(root_doc, client): """$history?limit=(more than revisions) works, removes pagination.""" rev_count = root_doc.revisions.count() - url = reverse('wiki.document_revisions', args=(root_doc.slug,)) + url = reverse("wiki.document_revisions", args=(root_doc.slug,)) limit_url = urlparams(url, limit=rev_count + 1) resp = client.get(limit_url, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 200 page = pq(resp.content) - assert len(page.find('ol.pagination')) == 0 + assert len(page.find("ol.pagination")) == 0 def test_revisions_request_invalid_pages(root_doc, client): """$history?limit=nonsense uses the default pagination.""" - url = reverse('wiki.document_revisions', args=(root_doc.slug,)) - limit_url = urlparams(url, limit='nonsense') + url = reverse("wiki.document_revisions", args=(root_doc.slug,)) + limit_url = urlparams(url, limit="nonsense") resp = client.get(limit_url, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 200 def test_list_no_redirects(redirect_doc, doc_hierarchy, client): - url = reverse('wiki.all_documents') + url = reverse("wiki.all_documents") resp = client.get(url, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 200 assert_shared_cache_header(resp) - assert 'text/html' in resp['Content-Type'] + assert "text/html" in resp["Content-Type"] # There should be 4 documents in the 'en-US' locale from # doc_hierarchy, plus the root_doc (which is pulled-in by # the redirect_doc), but the redirect_doc should not be one of them. - assert len(pq(resp.content).find('.document-list li')) == 5 + assert len(pq(resp.content).find(".document-list li")) == 5 assert redirect_doc.slug.encode() not in resp.content def test_tags(root_doc, client): """Test list of all tags.""" - root_doc.tags.set('foobar', 'blast') - url = reverse('wiki.list_tags') + root_doc.tags.set("foobar", "blast") + url = reverse("wiki.list_tags") resp = client.get(url, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 200 - assert b'foobar' in resp.content - assert b'blast' in resp.content - assert 'wiki/list/tags.html' in [t.name for t in resp.templates] + assert b"foobar" in resp.content + assert b"blast" in resp.content + assert "wiki/list/tags.html" in [t.name for t in resp.templates] assert_shared_cache_header(resp) @pytest.mark.tags -@pytest.mark.parametrize('tag', ['foo', 'bar']) -@pytest.mark.parametrize('tag_case', ['lower', 'upper']) -@pytest.mark.parametrize('locale_case', ['root', 'trans']) +@pytest.mark.parametrize("tag", ["foo", "bar"]) +@pytest.mark.parametrize("tag_case", ["lower", "upper"]) +@pytest.mark.parametrize("locale_case", ["root", "trans"]) def test_tag_list(root_doc, trans_doc, client, locale_case, tag_case, tag): """ Verify the tagged documents list view. Tags should be case insensitive (https://bugzil.la/976071). """ tag_query = getattr(tag, tag_case)() - root_doc.tags.set('foo', 'bar') - trans_doc.tags.set('foo', 'bar') - exp_doc = root_doc if (locale_case == 'root') else trans_doc - url = reverse('wiki.tag', locale=exp_doc.locale, kwargs={'tag': tag_query}) + root_doc.tags.set("foo", "bar") + trans_doc.tags.set("foo", "bar") + exp_doc = root_doc if (locale_case == "root") else trans_doc + url = reverse("wiki.tag", locale=exp_doc.locale, kwargs={"tag": tag_query}) resp = client.get(url, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 200 assert_shared_cache_header(resp) dom = pq(resp.content) selector = 'ul.document-list li a[href="/{}/docs/{}"]' - assert len(dom('#document-list ul.document-list li')) == 1 + assert len(dom("#document-list ul.document-list li")) == 1 assert len(dom.find(selector.format(exp_doc.locale, exp_doc.slug))) == 1 # Changing the tags to something other than what we're # searching for should take the results to zero. - root_doc.tags.set('foobar') - trans_doc.tags.set('foobar') + root_doc.tags.set("foobar") + trans_doc.tags.set("foobar") resp = client.get(url, HTTP_HOST=settings.WIKI_HOST) assert resp.status_code == 200 dom = pq(resp.content) - assert len(dom('#document-list ul.document-list li')) == 0 + assert len(dom("#document-list ul.document-list li")) == 0 assert root_doc.slug not in resp.content.decode(resp.charset) assert trans_doc.slug not in resp.content.decode(resp.charset) -@pytest.mark.parametrize('locale', ['en-US', 'de', 'fr']) +@pytest.mark.parametrize("locale", ["en-US", "de", "fr"]) def test_list_with_errors(redirect_doc, doc_hierarchy, client, locale): top_doc = doc_hierarchy.top bottom_doc = doc_hierarchy.bottom - de_doc = top_doc.translated_to('de') + de_doc = top_doc.translated_to("de") for doc in (top_doc, bottom_doc, de_doc, redirect_doc): - doc.rendered_errors = 'bad render' + doc.rendered_errors = "bad render" doc.save() - if locale == 'en-US': + if locale == "en-US": exp_docs = (top_doc, bottom_doc) - elif locale == 'de': + elif locale == "de": exp_docs = (de_doc,) else: # fr exp_docs = () - url = reverse('wiki.errors', locale=locale) + url = reverse("wiki.errors", locale=locale) resp = client.get(url, HTTP_HOST=settings.WIKI_HOST) dom = pq(resp.content) assert resp.status_code == 200 assert_shared_cache_header(resp) - assert 'text/html' in resp['Content-Type'] - assert len(dom.find('.document-list li')) == len(exp_docs) + assert "text/html" in resp["Content-Type"] + assert len(dom.find(".document-list li")) == len(exp_docs) selector = 'ul.document-list li a[href="/{}/docs/{}"]' for doc in exp_docs: assert len(dom.find(selector.format(doc.locale, doc.slug))) == 1 -@pytest.mark.parametrize('locale', ['en-US', 'de', 'fr']) -def test_list_without_parent(redirect_doc, root_doc, doc_hierarchy, client, - locale): - if locale == 'en-US': - exp_docs = (root_doc, - doc_hierarchy.top, - doc_hierarchy.middle_top, - doc_hierarchy.middle_bottom, - doc_hierarchy.bottom) +@pytest.mark.parametrize("locale", ["en-US", "de", "fr"]) +def test_list_without_parent(redirect_doc, root_doc, doc_hierarchy, client, locale): + if locale == "en-US": + exp_docs = ( + root_doc, + doc_hierarchy.top, + doc_hierarchy.middle_top, + doc_hierarchy.middle_bottom, + doc_hierarchy.bottom, + ) else: # All translations have a parent. exp_docs = () - url = reverse('wiki.without_parent', locale=locale) + url = reverse("wiki.without_parent", locale=locale) resp = client.get(url, HTTP_HOST=settings.WIKI_HOST) dom = pq(resp.content) assert resp.status_code == 200 assert_shared_cache_header(resp) - assert 'text/html' in resp['Content-Type'] - assert len(dom.find('.document-list li')) == len(exp_docs) + assert "text/html" in resp["Content-Type"] + assert len(dom.find(".document-list li")) == len(exp_docs) selector = 'ul.document-list li a[href="/{}/docs/{}"]' for doc in exp_docs: assert len(dom.find(selector.format(doc.locale, doc.slug))) == 1 -@pytest.mark.parametrize('locale', ['en-US', 'de', 'fr']) +@pytest.mark.parametrize("locale", ["en-US", "de", "fr"]) def test_list_top_level(redirect_doc, root_doc, doc_hierarchy, client, locale): - if locale == 'en-US': + if locale == "en-US": exp_docs = (root_doc, doc_hierarchy.top) else: exp_docs = (doc_hierarchy.top.translated_to(locale),) - url = reverse('wiki.top_level', locale=locale) + url = reverse("wiki.top_level", locale=locale) resp = client.get(url, HTTP_HOST=settings.WIKI_HOST) dom = pq(resp.content) assert resp.status_code == 200 assert_shared_cache_header(resp) - assert 'text/html' in resp['Content-Type'] - assert len(dom.find('.document-list li')) == len(exp_docs) + assert "text/html" in resp["Content-Type"] + assert len(dom.find(".document-list li")) == len(exp_docs) selector = 'ul.document-list li a[href="/{}/docs/{}"]' for doc in exp_docs: assert len(dom.find(selector.format(doc.locale, doc.slug))) == 1 -@pytest.mark.parametrize('locale', ['en-US', 'de', 'fr']) -def test_list_with_localization_tag(redirect_doc, doc_hierarchy, client, - locale): +@pytest.mark.parametrize("locale", ["en-US", "de", "fr"]) +def test_list_with_localization_tag(redirect_doc, doc_hierarchy, client, locale): top_doc = doc_hierarchy.top bottom_doc = doc_hierarchy.bottom - de_doc = top_doc.translated_to('de') + de_doc = top_doc.translated_to("de") for doc in (top_doc, bottom_doc, de_doc, redirect_doc): - doc.current_revision.localization_tags.set('inprogress') + doc.current_revision.localization_tags.set("inprogress") - if locale == 'en-US': + if locale == "en-US": exp_docs = (top_doc, bottom_doc) - elif locale == 'de': + elif locale == "de": exp_docs = (de_doc,) else: # fr exp_docs = () - url = reverse('wiki.list_with_localization_tag', locale=locale, - kwargs={'tag': 'inprogress'}) + url = reverse( + "wiki.list_with_localization_tag", locale=locale, kwargs={"tag": "inprogress"} + ) resp = client.get(url, HTTP_HOST=settings.WIKI_HOST) dom = pq(resp.content) assert resp.status_code == 200 assert_shared_cache_header(resp) - assert 'text/html' in resp['Content-Type'] - assert len(dom.find('.document-list li')) == len(exp_docs) + assert "text/html" in resp["Content-Type"] + assert len(dom.find(".document-list li")) == len(exp_docs) selector = 'ul.document-list li a[href="/{}/docs/{}"]' for doc in exp_docs: assert len(dom.find(selector.format(doc.locale, doc.slug))) == 1 -@pytest.mark.parametrize('locale', ['en-US', 'de', 'fr']) -def test_list_with_localization_tags(redirect_doc, doc_hierarchy, client, - locale): +@pytest.mark.parametrize("locale", ["en-US", "de", "fr"]) +def test_list_with_localization_tags(redirect_doc, doc_hierarchy, client, locale): top_doc = doc_hierarchy.top bottom_doc = doc_hierarchy.bottom - de_doc = top_doc.translated_to('de') + de_doc = top_doc.translated_to("de") for doc in (top_doc, bottom_doc, de_doc, redirect_doc): - doc.current_revision.localization_tags.set('inprogress') + doc.current_revision.localization_tags.set("inprogress") - if locale == 'en-US': + if locale == "en-US": exp_docs = (top_doc, bottom_doc) - elif locale == 'de': + elif locale == "de": exp_docs = (de_doc,) else: # fr exp_docs = () - url = reverse('wiki.list_with_localization_tags', locale=locale) + url = reverse("wiki.list_with_localization_tags", locale=locale) resp = client.get(url, HTTP_HOST=settings.WIKI_HOST) dom = pq(resp.content) assert resp.status_code == 200 assert_shared_cache_header(resp) - assert 'text/html' in resp['Content-Type'] - assert len(dom.find('.document-list li')) == len(exp_docs) + assert "text/html" in resp["Content-Type"] + assert len(dom.find(".document-list li")) == len(exp_docs) selector = 'ul.document-list li a[href="/{}/docs/{}"]' for doc in exp_docs: assert len(dom.find(selector.format(doc.locale, doc.slug))) == 1 diff --git a/kuma/wiki/tests/test_views_misc.py b/kuma/wiki/tests/test_views_misc.py index d64ca53114f..59eb0b6f1df 100644 --- a/kuma/wiki/tests/test_views_misc.py +++ b/kuma/wiki/tests/test_views_misc.py @@ -9,32 +9,29 @@ from kuma.core.urlresolvers import reverse -@pytest.mark.parametrize( - 'http_method', ['put', 'post', 'delete', 'options', 'head']) -@pytest.mark.parametrize( - 'endpoint', ['ckeditor_config', 'autosuggest_documents']) +@pytest.mark.parametrize("http_method", ["put", "post", "delete", "options", "head"]) +@pytest.mark.parametrize("endpoint", ["ckeditor_config", "autosuggest_documents"]) def test_disallowed_methods(db, client, http_method, endpoint): """HTTP methods other than GET & HEAD are not allowed.""" - url = reverse('wiki.{}'.format(endpoint)) + url = reverse("wiki.{}".format(endpoint)) response = getattr(client, http_method)(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 405 assert_shared_cache_header(response) def test_ckeditor_config(db, client): - response = client.get(reverse('wiki.ckeditor_config'), - HTTP_HOST=settings.WIKI_HOST) + response = client.get(reverse("wiki.ckeditor_config"), HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 assert_shared_cache_header(response) - assert response['Content-Type'] == 'application/x-javascript' - assert 'wiki/ckeditor_config.js' in [t.name for t in response.templates] + assert response["Content-Type"] == "application/x-javascript" + assert "wiki/ckeditor_config.js" in [t.name for t in response.templates] -@pytest.mark.parametrize('term', [None, 'doc']) +@pytest.mark.parametrize("term", [None, "doc"]) @pytest.mark.parametrize( - 'locale_case', - ['all-locales', 'current-locale', - 'non-english-locale', 'exclude-current-locale']) + "locale_case", + ["all-locales", "current-locale", "non-english-locale", "exclude-current-locale"], +) def test_autosuggest(client, redirect_doc, doc_hierarchy, locale_case, term): params = {} expected_status_code = 200 @@ -42,35 +39,44 @@ def test_autosuggest(client, redirect_doc, doc_hierarchy, locale_case, term): params.update(term=term) else: expected_status_code = 400 - if locale_case == 'non-english-locale': - params.update(locale='it') - expected_titles = {'Superiore Documento'} - elif locale_case == 'current-locale': - params.update(current_locale='true') + if locale_case == "non-english-locale": + params.update(locale="it") + expected_titles = {"Superiore Documento"} + elif locale_case == "current-locale": + params.update(current_locale="true") # The root document is pulled-in by the redirect_doc fixture. - expected_titles = {'Root Document', 'Top Document', - 'Middle-Top Document', 'Middle-Bottom Document', - 'Bottom Document'} - elif locale_case == 'exclude-current-locale': - params.update(exclude_current_locale='true') - expected_titles = {'Haut Document', 'Superiore Documento'} + expected_titles = { + "Root Document", + "Top Document", + "Middle-Top Document", + "Middle-Bottom Document", + "Bottom Document", + } + elif locale_case == "exclude-current-locale": + params.update(exclude_current_locale="true") + expected_titles = {"Haut Document", "Superiore Documento"} else: # All locales # The root document is pulled-in by the redirect_doc fixture. - expected_titles = {'Root Document', 'Top Document', - 'Haut Document', 'Superiore Documento', - 'Middle-Top Document', 'Middle-Bottom Document', - 'Bottom Document'} + expected_titles = { + "Root Document", + "Top Document", + "Haut Document", + "Superiore Documento", + "Middle-Top Document", + "Middle-Bottom Document", + "Bottom Document", + } - url = reverse('wiki.autosuggest_documents') + url = reverse("wiki.autosuggest_documents") if params: - url += '?{}'.format(urlencode(params)) - with override_switch('application_ACAO', True): + url += "?{}".format(urlencode(params)) + with override_switch("application_ACAO", True): response = client.get(url) assert response.status_code == expected_status_code assert_shared_cache_header(response) - assert 'Access-Control-Allow-Origin' in response - assert response['Access-Control-Allow-Origin'] == '*' + assert "Access-Control-Allow-Origin" in response + assert response["Access-Control-Allow-Origin"] == "*" if expected_status_code == 200: - assert response['Content-Type'] == 'application/json' + assert response["Content-Type"] == "application/json" data = json.loads(response.content) - assert set(item['title'] for item in data) == expected_titles + assert set(item["title"] for item in data) == expected_titles diff --git a/kuma/wiki/tests/test_views_revision.py b/kuma/wiki/tests/test_views_revision.py index 2c6d4796fef..cdbeb77b0b4 100644 --- a/kuma/wiki/tests/test_views_revision.py +++ b/kuma/wiki/tests/test_views_revision.py @@ -18,13 +18,15 @@ def doc_with_macros(wiki_user): """A top-level English document containing multiple macro calls.""" doc_with_macros = Document.objects.create( - locale='en-US', slug='Macros', title='Macros Document') + locale="en-US", slug="Macros", title="Macros Document" + ) Revision.objects.create( document=doc_with_macros, creator=wiki_user, content='{{M1("x")}}{{M1("y")}}{{M2("z")}}', - title='Macros Document', - created=datetime(2020, 1, 11, 10, 15)) + title="Macros Document", + created=datetime(2020, 1, 11, 10, 15), + ) return doc_with_macros @@ -33,43 +35,44 @@ def wiki_user_2_token(wiki_user_2): return Token.objects.create(user=wiki_user_2) -@pytest.mark.parametrize('raw', [True, False]) +@pytest.mark.parametrize("raw", [True, False]) def test_compare_revisions(edit_revision, client, raw): """Comparing two valid revisions of the same document works.""" doc = edit_revision.document first_revision = doc.revisions.first() - params = {'from': first_revision.id, 'to': edit_revision.id} + params = {"from": first_revision.id, "to": edit_revision.id} if raw: - params['raw'] = '1' - url = urlparams(reverse('wiki.compare_revisions', args=[doc.slug]), - **params) + params["raw"] = "1" + url = urlparams(reverse("wiki.compare_revisions", args=[doc.slug]), **params) response = client.get(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 - assert response['X-Robots-Tag'] == 'noindex' + assert response["X-Robots-Tag"] == "noindex" assert_shared_cache_header(response) -@pytest.mark.parametrize('raw', [True, False]) +@pytest.mark.parametrize("raw", [True, False]) def test_compare_translation(trans_revision, client, raw): """A localized revision can be compared to an English source revision.""" fr_doc = trans_revision.document en_revision = trans_revision.based_on en_doc = en_revision.document assert en_doc != fr_doc - params = {'from': en_revision.id, 'to': trans_revision.id} + params = {"from": en_revision.id, "to": trans_revision.id} if raw: - params['raw'] = '1' - url = urlparams(reverse('wiki.compare_revisions', args=[fr_doc.slug], - locale=fr_doc.locale), **params) + params["raw"] = "1" + url = urlparams( + reverse("wiki.compare_revisions", args=[fr_doc.slug], locale=fr_doc.locale), + **params, + ) response = client.get(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 - assert response['X-Robots-Tag'] == 'noindex' + assert response["X-Robots-Tag"] == "noindex" assert_shared_cache_header(response) -@pytest.mark.parametrize('raw', [True, False]) +@pytest.mark.parametrize("raw", [True, False]) def test_compare_revisions_without_tidied_content(edit_revision, client, raw): """Comparing revisions without tidied content displays a wait message.""" doc = edit_revision.document @@ -77,38 +80,39 @@ def test_compare_revisions_without_tidied_content(edit_revision, client, raw): # update() to skip the tidy_revision_content post_save signal handler ids = [first_revision.id, edit_revision.id] - Revision.objects.filter(id__in=ids).update(tidied_content='') + Revision.objects.filter(id__in=ids).update(tidied_content="") - params = {'from': first_revision.id, 'to': edit_revision.id} + params = {"from": first_revision.id, "to": edit_revision.id} if raw: - params['raw'] = '1' - url = urlparams(reverse('wiki.compare_revisions', args=[doc.slug]), - **params) + params["raw"] = "1" + url = urlparams(reverse("wiki.compare_revisions", args=[doc.slug]), **params) response = client.get(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 - assert b'Please refresh this page in a few minutes.' in response.content + assert b"Please refresh this page in a few minutes." in response.content -@pytest.mark.parametrize("id1,id2", - [('1e309', '1e309'), - ('', 'invalid'), - ('invalid', ''), - ]) +@pytest.mark.parametrize( + "id1,id2", [("1e309", "1e309"), ("", "invalid"), ("invalid", "")] +) def test_compare_revisions_invalid_ids(root_doc, client, id1, id2): """Comparing badly-formed revision parameters return 404, not error.""" - url = urlparams(reverse('wiki.compare_revisions', args=[root_doc.slug]), - **{'from': id1, 'to': id2}) + url = urlparams( + reverse("wiki.compare_revisions", args=[root_doc.slug]), + **{"from": id1, "to": id2}, + ) response = client.get(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 404 -@pytest.mark.parametrize('param', ['from', 'to']) +@pytest.mark.parametrize("param", ["from", "to"]) def test_compare_revisions_only_one_param(create_revision, client, param): """If a compare query parameter is missing, a 404 is returned.""" doc = create_revision.document - url = urlparams(reverse('wiki.compare_revisions', args=[doc.slug]), - **{param: create_revision.id}) + url = urlparams( + reverse("wiki.compare_revisions", args=[doc.slug]), + **{param: create_revision.id}, + ) response = client.get(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 404 @@ -117,21 +121,24 @@ def test_compare_revisions_wrong_document(edit_revision, client): """If the revision is for the wrong document, a 404 is returned.""" doc = edit_revision.document first_revision = doc.revisions.first() - other_doc = Document.objects.create(locale='en-US', slug='Other', - title='Other Document') - url = urlparams(reverse('wiki.compare_revisions', args=[other_doc.slug]), - **{'from': first_revision.id, 'to': edit_revision.id}) + other_doc = Document.objects.create( + locale="en-US", slug="Other", title="Other Document" + ) + url = urlparams( + reverse("wiki.compare_revisions", args=[other_doc.slug]), + **{"from": first_revision.id, "to": edit_revision.id}, + ) response = client.get(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 404 -@pytest.mark.parametrize('http_method', ('put', 'delete')) +@pytest.mark.parametrize("http_method", ("put", "delete")) def test_revision_api_disallowed_methods(client, http_method): """ The wiki.revision_api endpoint does not support HTTP methods other than GET, HEAD, OPTIONS, and POST. """ - url = reverse('wiki.revision_api', args=['Web/HTML'], locale='fr') + url = reverse("wiki.revision_api", args=["Web/HTML"], locale="fr") response = getattr(client, http_method)(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 405 assert_no_cache_header(response) @@ -139,10 +146,10 @@ def test_revision_api_disallowed_methods(client, http_method): def test_revision_api_ensure_wiki_domain(client): """The wiki.revision_api endpoint is only supported on the wiki domain.""" - url = reverse('wiki.revision_api', args=['Web/HTML'], locale='fr') + url = reverse("wiki.revision_api", args=["Web/HTML"], locale="fr") response = client.get(url) assert response.status_code == 301 - assert response['Location'].startswith(settings.WIKI_SITE_URL) + assert response["Location"].startswith(settings.WIKI_SITE_URL) assert_no_cache_header(response) @@ -150,22 +157,32 @@ def test_revision_api_404(db, client): """ The wiki.revision_api endpoint returns 404 if the document does not exist. """ - url = reverse('wiki.revision_api', args=['does/not/exist'], locale='de') + url = reverse("wiki.revision_api", args=["does/not/exist"], locale="de") response = client.get(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 404 assert_no_cache_header(response) -@pytest.mark.parametrize('qs,expected', ( - ('?macros=m1,m2', 'Please specify a "mode" query parameter.'), - ('?mode=sing', 'The "mode" query parameter must be "render" or "remove".'), - ('?mode=remove', ('Please specify one or more comma-separated macro names ' - 'via the "macros" query parameter.')), -), ids=('missing-mode', 'invalid-mode', 'missing-macros')) +@pytest.mark.parametrize( + "qs,expected", + ( + ("?macros=m1,m2", 'Please specify a "mode" query parameter.'), + ("?mode=sing", 'The "mode" query parameter must be "render" or "remove".'), + ( + "?mode=remove", + ( + "Please specify one or more comma-separated macro names " + 'via the "macros" query parameter.' + ), + ), + ), + ids=("missing-mode", "invalid-mode", "missing-macros"), +) def test_revision_api_get_400(doc_with_macros, client, qs, expected): """The wiki.revision_api endpoint returns 400 for bad GET requests.""" - url = reverse('wiki.revision_api', args=[doc_with_macros.slug], - locale=doc_with_macros.locale) + url = reverse( + "wiki.revision_api", args=[doc_with_macros.slug], locale=doc_with_macros.locale + ) response = client.get(url + qs, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 400 assert response.content.decode() == expected @@ -176,9 +193,10 @@ def test_revision_api_post_unauthorized(doc_with_macros, client): """ The wiki.revision_api endpoint returns 403 for unauthorized POST requests. """ - url = reverse('wiki.revision_api', args=[doc_with_macros.slug], - locale=doc_with_macros.locale) - data = dict(content='yada yada yada') + url = reverse( + "wiki.revision_api", args=[doc_with_macros.slug], locale=doc_with_macros.locale + ) + data = dict(content="yada yada yada") response = client.post(url, data=data, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 403 assert_no_cache_header(response) @@ -186,13 +204,14 @@ def test_revision_api_post_unauthorized(doc_with_macros, client): def test_revision_api_post_400(doc_with_macros, wiki_user_2_token, client): """The wiki.revision_api endpoint returns 400 for bad POST requests.""" - url = reverse('wiki.revision_api', args=[doc_with_macros.slug], - locale=doc_with_macros.locale) + url = reverse( + "wiki.revision_api", args=[doc_with_macros.slug], locale=doc_with_macros.locale + ) response = client.post( url, - content_type='text/plain', + content_type="text/plain", HTTP_HOST=settings.WIKI_HOST, - HTTP_AUTHORIZATION=f'Token {wiki_user_2_token.key}' + HTTP_AUTHORIZATION=f"Token {wiki_user_2_token.key}", ) assert response.status_code == 400 assert response.content.decode() == ( @@ -202,86 +221,92 @@ def test_revision_api_post_400(doc_with_macros, wiki_user_2_token, client): assert_no_cache_header(response) -@pytest.mark.parametrize('qs,expected', ( - ('', '{{M1("x")}}{{M1("y")}}{{M2("z")}}'), - ('?mode=remove¯os=m1', '{{M2("z")}}'), - ('?mode=remove¯os=m1,m2', ''), - ('?mode=remove¯os=junk', '{{M1("x")}}{{M1("y")}}{{M2("z")}}'), - ('?mode=render¯os=junk', '{{M1("x")}}{{M1("y")}}{{M2("z")}}')), - ids=('no-change', 'remove-single', 'remove-multiple', 'remove-miss', - 'render-miss') +@pytest.mark.parametrize( + "qs,expected", + ( + ("", '{{M1("x")}}{{M1("y")}}{{M2("z")}}'), + ("?mode=remove¯os=m1", '{{M2("z")}}'), + ("?mode=remove¯os=m1,m2", ""), + ("?mode=remove¯os=junk", '{{M1("x")}}{{M1("y")}}{{M2("z")}}'), + ("?mode=render¯os=junk", '{{M1("x")}}{{M1("y")}}{{M2("z")}}'), + ), + ids=("no-change", "remove-single", "remove-multiple", "remove-miss", "render-miss"), ) -def test_revision_api_get(doc_with_macros, client, constance_config, qs, - expected): +def test_revision_api_get(doc_with_macros, client, constance_config, qs, expected): """ The wiki.revision_api endpoint returns revised raw HTML for GET requests. """ constance_config.KUMASCRIPT_TIMEOUT = 1 - url = reverse('wiki.revision_api', args=[doc_with_macros.slug], - locale=doc_with_macros.locale) + url = reverse( + "wiki.revision_api", args=[doc_with_macros.slug], locale=doc_with_macros.locale + ) response = client.get(url + qs, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 assert response.content.decode() == expected assert_no_cache_header(response) - assert response['X-Frame-Options'] == 'deny' - assert response['X-Robots-Tag'] == 'noindex' - assert response['ETag'] == f'"{str(doc_with_macros.current_revision.id)}"' + assert response["X-Frame-Options"] == "deny" + assert response["X-Robots-Tag"] == "noindex" + assert response["ETag"] == f'"{str(doc_with_macros.current_revision.id)}"' -@pytest.mark.parametrize('case', ('json', 'form-data', 'form-urlencoded')) -def test_revision_api_post(doc_with_macros, wiki_user_2, wiki_user_2_token, - client, case): +@pytest.mark.parametrize("case", ("json", "form-data", "form-urlencoded")) +def test_revision_api_post( + doc_with_macros, wiki_user_2, wiki_user_2_token, client, case +): """ The wiki.revision_api endpoint returns 201 for successful POST requests. """ - url = reverse('wiki.revision_api', args=[doc_with_macros.slug], - locale=doc_with_macros.locale) + url = reverse( + "wiki.revision_api", args=[doc_with_macros.slug], locale=doc_with_macros.locale + ) kwargs = dict( - data=dict(content='yada'), + data=dict(content="yada"), HTTP_HOST=settings.WIKI_HOST, - HTTP_AUTHORIZATION=f'Token {wiki_user_2_token.key}' + HTTP_AUTHORIZATION=f"Token {wiki_user_2_token.key}", ) - if case == 'json': - kwargs['data'] = json.dumps(kwargs['data']) - kwargs.update(content_type='application/json') - elif case == 'form-urlencoded': - kwargs['data'] = urlencode(kwargs['data']) - kwargs.update(content_type='application/x-www-form-urlencoded') + if case == "json": + kwargs["data"] = json.dumps(kwargs["data"]) + kwargs.update(content_type="application/json") + elif case == "form-urlencoded": + kwargs["data"] = urlencode(kwargs["data"]) + kwargs.update(content_type="application/x-www-form-urlencoded") response = client.post(url, **kwargs) doc_with_macros.refresh_from_db() new_rev = doc_with_macros.current_revision assert new_rev.creator == wiki_user_2 - assert new_rev.content == 'yada' + assert new_rev.content == "yada" assert response.status_code == 201 - assert response['Location'].endswith(reverse( - 'wiki.revision', - args=(doc_with_macros.slug, new_rev.id), - locale=doc_with_macros.locale - )) - assert response.content.decode() == 'yada' - assert response['ETag'] == f'"{str(new_rev.id)}"' + assert response["Location"].endswith( + reverse( + "wiki.revision", + args=(doc_with_macros.slug, new_rev.id), + locale=doc_with_macros.locale, + ) + ) + assert response.content.decode() == "yada" + assert response["ETag"] == f'"{str(new_rev.id)}"' assert_no_cache_header(response) -def test_revision_api_conditional_post(doc_with_macros, wiki_user_2, - wiki_user_2_token, constance_config, - client): +def test_revision_api_conditional_post( + doc_with_macros, wiki_user_2, wiki_user_2_token, constance_config, client +): """ The wiki.revision_api endpoint returns 201 for successful, and 412 for failed, conditional POST requests. """ constance_config.KUMASCRIPT_TIMEOUT = 1 - url = reverse('wiki.revision_api', args=[doc_with_macros.slug], - locale=doc_with_macros.locale) + url = reverse( + "wiki.revision_api", args=[doc_with_macros.slug], locale=doc_with_macros.locale + ) # First let's get some revised content and the ETag header. - response = client.get( - url + '?mode=remove¯os=m1', HTTP_HOST=settings.WIKI_HOST) + response = client.get(url + "?mode=remove¯os=m1", HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 revised_content = response.content.decode() assert revised_content == '{{M2("z")}}' - etag_from_get = response['ETag'] + etag_from_get = response["ETag"] # Let's POST the revised content, but with the condition that no one else # has created a new revision for the document since we performed our GET. @@ -290,21 +315,23 @@ def test_revision_api_conditional_post(doc_with_macros, wiki_user_2, data=dict(content=revised_content), HTTP_HOST=settings.WIKI_HOST, HTTP_IF_MATCH=etag_from_get, - HTTP_AUTHORIZATION=f'Token {wiki_user_2_token.key}' + HTTP_AUTHORIZATION=f"Token {wiki_user_2_token.key}", ) doc_with_macros.refresh_from_db() new_rev = doc_with_macros.current_revision assert new_rev.creator == wiki_user_2 assert new_rev.content == revised_content assert response.status_code == 201 - assert response['Location'].endswith(reverse( - 'wiki.revision', - args=(doc_with_macros.slug, new_rev.id), - locale=doc_with_macros.locale - )) + assert response["Location"].endswith( + reverse( + "wiki.revision", + args=(doc_with_macros.slug, new_rev.id), + locale=doc_with_macros.locale, + ) + ) assert response.content.decode() == revised_content assert_no_cache_header(response) - assert response['ETag'] == f'"{str(new_rev.id)}"' + assert response["ETag"] == f'"{str(new_rev.id)}"' # Now let's pretend we're someone else, holding the same "etag_from_get" # value, who also wants to conditionally revise the document, but since @@ -312,10 +339,10 @@ def test_revision_api_conditional_post(doc_with_macros, wiki_user_2, # fail. response = client.post( url, - data=dict(content='yada'), + data=dict(content="yada"), HTTP_HOST=settings.WIKI_HOST, HTTP_IF_MATCH=etag_from_get, - HTTP_AUTHORIZATION=f'Token {wiki_user_2_token.key}' + HTTP_AUTHORIZATION=f"Token {wiki_user_2_token.key}", ) assert response.status_code == 412 assert_no_cache_header(response) diff --git a/kuma/wiki/tests/test_views_translate.py b/kuma/wiki/tests/test_views_translate.py index 5cf62c901b4..2258a25b139 100644 --- a/kuma/wiki/tests/test_views_translate.py +++ b/kuma/wiki/tests/test_views_translate.py @@ -1,5 +1,3 @@ - - import pytest from django.conf import settings from django.contrib.auth.models import Permission @@ -13,7 +11,7 @@ @pytest.fixture def permission_change_document(db): - return Permission.objects.get(codename='change_document') + return Permission.objects.get(codename="change_document") @pytest.fixture @@ -25,23 +23,23 @@ def trans_doc_client(editor_client, wiki_user, permission_change_document): def test_translate_get(root_doc, trans_doc_client): """Test GET on the translate view.""" - url = reverse('wiki.translate', args=(root_doc.slug,)) - url += '?tolocale=Fr' + url = reverse("wiki.translate", args=(root_doc.slug,)) + url += "?tolocale=Fr" response = trans_doc_client.get(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 200 - assert response['X-Robots-Tag'] == 'noindex' + assert response["X-Robots-Tag"] == "noindex" assert_no_cache_header(response) page = pq(response.content) - assert page.find('input[name=slug]')[0].value == root_doc.slug + assert page.find("input[name=slug]")[0].value == root_doc.slug def test_translate_get_invalid_locale(root_doc, trans_doc_client): """Test GET on the translate view but with an invalid 'tolocale' query string parameter.""" - url = reverse('wiki.translate', args=(root_doc.slug,)) - url += '?tolocale=XxX' + url = reverse("wiki.translate", args=(root_doc.slug,)) + url += "?tolocale=XxX" response = trans_doc_client.get(url, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 404 @@ -51,23 +49,25 @@ def test_translate_post(root_doc, trans_doc_client): """Test POST on the translate view.""" data = { - 'slug': root_doc.slug, - 'title': root_doc.title, - 'content': root_doc.current_revision.content, - 'form-type': 'both', - 'toc_depth': 1 + "slug": root_doc.slug, + "title": root_doc.title, + "content": root_doc.current_revision.content, + "form-type": "both", + "toc_depth": 1, } - url = reverse('wiki.translate', args=(root_doc.slug,)) - url += '?tolocale=fr' + url = reverse("wiki.translate", args=(root_doc.slug,)) + url += "?tolocale=fr" response = trans_doc_client.post(url, data, HTTP_HOST=settings.WIKI_HOST) assert response.status_code == 302 - assert response['X-Robots-Tag'] == 'noindex' + assert response["X-Robots-Tag"] == "noindex" assert_no_cache_header(response) - doc_url = reverse('wiki.document', args=(root_doc.slug,), locale='fr') - assert doc_url + '?rev_saved=' in response['Location'] - assert len(Document.objects.filter(locale='fr', slug=root_doc.slug)) == 1 + doc_url = reverse("wiki.document", args=(root_doc.slug,), locale="fr") + assert doc_url + "?rev_saved=" in response["Location"] + assert len(Document.objects.filter(locale="fr", slug=root_doc.slug)) == 1 # Ensure there is no redirect. - assert len(Document.objects.filter( - title=root_doc.title + ' Redirect 1', locale='fr')) == 0 + assert ( + len(Document.objects.filter(title=root_doc.title + " Redirect 1", locale="fr")) + == 0 + ) diff --git a/kuma/wiki/urls.py b/kuma/wiki/urls.py index 661c7501292..30cee41b121 100644 --- a/kuma/wiki/urls.py +++ b/kuma/wiki/urls.py @@ -1,5 +1,3 @@ - - from django.conf.urls import include, url from django.views.generic import RedirectView @@ -13,170 +11,148 @@ # These patterns inherit (?P[^\$]+). document_patterns = [ - url(r'^$', - views.document.document, - name='wiki.document'), - url(r'^\$api$', - views.document.document_api, - name='wiki.document_api'), - url(r'^\$revision$', - views.revision.revision_api, - name='wiki.revision_api'), - url(r'^\$revision/(?P\d+)$', + url(r"^$", views.document.document, name="wiki.document"), + url(r"^\$api$", views.document.document_api, name="wiki.document_api"), + url(r"^\$revision$", views.revision.revision_api, name="wiki.revision_api"), + url( + r"^\$revision/(?P\d+)$", views.revision.revision, - name='wiki.revision'), - url(r'^\$history$', - views.list.revisions, - name='wiki.document_revisions'), - url(r'^\$edit$', - views.edit.edit, - name='wiki.edit'), - url(r'^\$files$', - edit_attachment, - name='attachments.edit_attachment'), - url(r'^\$compare$', - views.revision.compare, - name='wiki.compare_revisions'), - url(r'^\$children$', - views.document.children, - name='wiki.children'), - url(r'^\$translate$', - views.translate.translate, - name='wiki.translate'), - url(r'^\$locales$', - views.translate.select_locale, - name='wiki.select_locale'), - url(r'^\$json$', - views.document.as_json, - name='wiki.json_slug'), - url(r'^\$toc$', - views.document.toc, - name='wiki.toc'), - url(r'^\$move$', - views.document.move, - name='wiki.move'), - url(r'^\$quick-review$', - views.revision.quick_review, - name='wiki.quick_review'), - url(r'^\$samples/(?P.+)/files/(?P\d+)/(?P.+)$', + name="wiki.revision", + ), + url(r"^\$history$", views.list.revisions, name="wiki.document_revisions"), + url(r"^\$edit$", views.edit.edit, name="wiki.edit"), + url(r"^\$files$", edit_attachment, name="attachments.edit_attachment"), + url(r"^\$compare$", views.revision.compare, name="wiki.compare_revisions"), + url(r"^\$children$", views.document.children, name="wiki.children"), + url(r"^\$translate$", views.translate.translate, name="wiki.translate"), + url(r"^\$locales$", views.translate.select_locale, name="wiki.select_locale"), + url(r"^\$json$", views.document.as_json, name="wiki.json_slug"), + url(r"^\$toc$", views.document.toc, name="wiki.toc"), + url(r"^\$move$", views.document.move, name="wiki.move"), + url(r"^\$quick-review$", views.revision.quick_review, name="wiki.quick_review"), + url( + r"^\$samples/(?P.+)/files/(?P\d+)/(?P.+)$", views.code.raw_code_sample_file, - name='wiki.raw_code_sample_file'), - url(r'^\$samples/(?P.+)$', + name="wiki.raw_code_sample_file", + ), + url( + r"^\$samples/(?P.+)$", views.code.code_sample, - name='wiki.code_sample'), - url(r'^\$revert/(?P\d+)$', + name="wiki.code_sample", + ), + url( + r"^\$revert/(?P\d+)$", views.delete.revert_document, - name='wiki.revert_document'), - url(r'^\$repair_breadcrumbs$', + name="wiki.revert_document", + ), + url( + r"^\$repair_breadcrumbs$", views.document.repair_breadcrumbs, - name='wiki.repair_breadcrumbs'), - url(r'^\$delete$', - views.delete.delete_document, - name='wiki.delete_document'), - url(r'^\$restore$', - views.delete.restore_document, - name='wiki.restore_document'), - url(r'^\$purge$', - views.delete.purge_document, - name='wiki.purge_document'), - + name="wiki.repair_breadcrumbs", + ), + url(r"^\$delete$", views.delete.delete_document, name="wiki.delete_document"), + url(r"^\$restore$", views.delete.restore_document, name="wiki.restore_document"), + url(r"^\$purge$", views.delete.purge_document, name="wiki.purge_document"), # Un/Subscribe to document edit notifications. - url(r'^\$subscribe$', - views.document.subscribe, - name='wiki.subscribe'), - + url(r"^\$subscribe$", views.document.subscribe, name="wiki.subscribe"), # Un/Subscribe to document tree edit notifications. - url(r'^\$subscribe_to_tree$', + url( + r"^\$subscribe_to_tree$", views.document.subscribe_to_tree, - name='wiki.subscribe_to_tree'), - + name="wiki.subscribe_to_tree", + ), ] non_document_patterns = [ - url(r'^ckeditor_config.js$', - views.misc.ckeditor_config, - name='wiki.ckeditor_config'), - + url( + r"^ckeditor_config.js$", views.misc.ckeditor_config, name="wiki.ckeditor_config" + ), # internals - url(r'^preview-wiki-content$', - views.revision.preview, - name='wiki.preview'), - url(r'^get-documents$', + url(r"^preview-wiki-content$", views.revision.preview, name="wiki.preview"), + url( + r"^get-documents$", views.misc.autosuggest_documents, - name='wiki.autosuggest_documents'), - + name="wiki.autosuggest_documents", + ), # Special pages - url(r'^tags$', - views.list.tags, - name='wiki.list_tags'), - url(r'^tag/(?P.+)$', - views.list.documents, - name='wiki.tag'), - url(r'^new$', - views.create.create, - name='wiki.create'), - url(r'^all$', - views.list.documents, - name='wiki.all_documents'), - url(r'^with-errors$', - views.list.with_errors, - name='wiki.errors'), - url(r'^without-parent$', - views.list.without_parent, - name='wiki.without_parent'), - url(r'^top-level$', - views.list.top_level, - name='wiki.top_level'), - url(r'^needs-review/(?P[^/]+)$', - views.list.needs_review, - name='wiki.list_review_tag'), - url(r'^needs-review/?', + url(r"^tags$", views.list.tags, name="wiki.list_tags"), + url(r"^tag/(?P.+)$", views.list.documents, name="wiki.tag"), + url(r"^new$", views.create.create, name="wiki.create"), + url(r"^all$", views.list.documents, name="wiki.all_documents"), + url(r"^with-errors$", views.list.with_errors, name="wiki.errors"), + url(r"^without-parent$", views.list.without_parent, name="wiki.without_parent"), + url(r"^top-level$", views.list.top_level, name="wiki.top_level"), + url( + r"^needs-review/(?P[^/]+)$", views.list.needs_review, - name='wiki.list_review'), - url(r'^localization-tag/(?P[^/]+)$', + name="wiki.list_review_tag", + ), + url(r"^needs-review/?", views.list.needs_review, name="wiki.list_review"), + url( + r"^localization-tag/(?P[^/]+)$", views.list.with_localization_tag, - name='wiki.list_with_localization_tag'), - url(r'^localization-tag/?', + name="wiki.list_with_localization_tag", + ), + url( + r"^localization-tag/?", views.list.with_localization_tag, - name='wiki.list_with_localization_tags'), - + name="wiki.list_with_localization_tags", + ), # Legacy KumaScript macro list, when they were stored in Kuma database - url(r'^templates$', - ensure_wiki_domain(shared_cache_control(s_maxage=60 * 60 * 24 * 30)( - RedirectView.as_view(pattern_name='dashboards.macros', - permanent=True) - ))), - + url( + r"^templates$", + ensure_wiki_domain( + shared_cache_control(s_maxage=60 * 60 * 24 * 30)( + RedirectView.as_view(pattern_name="dashboards.macros", permanent=True) + ) + ), + ), # Akismet Revision - url(r'^submit_akismet_spam$', + url( + r"^submit_akismet_spam$", views.akismet_revision.submit_akismet_spam, - name='wiki.submit_akismet_spam'), - + name="wiki.submit_akismet_spam", + ), # Feeds - url(r'^feeds/(?P[^/]+)/all/?', + url( + r"^feeds/(?P[^/]+)/all/?", shared_cache_control(feeds.DocumentsRecentFeed()), - name="wiki.feeds.recent_documents"), - url(r'^feeds/(?P[^/]+)/l10n-updates/?', + name="wiki.feeds.recent_documents", + ), + url( + r"^feeds/(?P[^/]+)/l10n-updates/?", shared_cache_control(feeds.DocumentsUpdatedTranslationParentFeed()), - name="wiki.feeds.l10n_updates"), - url(r'^feeds/(?P[^/]+)/tag/(?P[^/]+)', + name="wiki.feeds.l10n_updates", + ), + url( + r"^feeds/(?P[^/]+)/tag/(?P[^/]+)", shared_cache_control(feeds.DocumentsRecentFeed()), - name="wiki.feeds.recent_documents"), - url(r'^feeds/(?P[^/]+)/needs-review/(?P[^/]+)', + name="wiki.feeds.recent_documents", + ), + url( + r"^feeds/(?P[^/]+)/needs-review/(?P[^/]+)", shared_cache_control(feeds.DocumentsReviewFeed()), - name="wiki.feeds.list_review_tag"), - url(r'^feeds/(?P[^/]+)/needs-review/?', + name="wiki.feeds.list_review_tag", + ), + url( + r"^feeds/(?P[^/]+)/needs-review/?", shared_cache_control(feeds.DocumentsReviewFeed()), - name="wiki.feeds.list_review"), - url(r'^feeds/(?P[^/]+)/revisions/?', + name="wiki.feeds.list_review", + ), + url( + r"^feeds/(?P[^/]+)/revisions/?", shared_cache_control(feeds.RevisionsFeed()), - name="wiki.feeds.recent_revisions"), - url(r'^feeds/(?P[^/]+)/files/?', + name="wiki.feeds.recent_revisions", + ), + url( + r"^feeds/(?P[^/]+)/files/?", shared_cache_control(AttachmentsFeed()), - name="attachments.feeds.recent_files"), + name="attachments.feeds.recent_files", + ), ] lang_urlpatterns = non_document_patterns + [ - url(r'^(?P%s)' % DOCUMENT_PATH_RE.pattern, - include(document_patterns)), + url( + r"^(?P%s)" % DOCUMENT_PATH_RE.pattern, include(document_patterns) + ), ] diff --git a/kuma/wiki/urls_untrusted.py b/kuma/wiki/urls_untrusted.py index 844cabd1fc9..83a18fc1121 100644 --- a/kuma/wiki/urls_untrusted.py +++ b/kuma/wiki/urls_untrusted.py @@ -1,5 +1,3 @@ - - from django.conf.urls import include, url from . import views @@ -8,15 +6,20 @@ # These patterns inherit (?P[^\$]+). document_patterns = [ - url(r'^\$samples/(?P.+)/files/(?P\d+)/(?P.+)$', + url( + r"^\$samples/(?P.+)/files/(?P\d+)/(?P.+)$", views.code.raw_code_sample_file, - name='wiki.raw_code_sample_file'), - url(r'^\$samples/(?P.+)$', + name="wiki.raw_code_sample_file", + ), + url( + r"^\$samples/(?P.+)$", views.code.code_sample, - name='wiki.code_sample'), + name="wiki.code_sample", + ), ] lang_urlpatterns = [ - url(r'^(?P%s)' % DOCUMENT_PATH_RE.pattern, - include(document_patterns)), + url( + r"^(?P%s)" % DOCUMENT_PATH_RE.pattern, include(document_patterns) + ), ] diff --git a/kuma/wiki/utils.py b/kuma/wiki/utils.py index c661a27d7f3..457eafcdd8e 100644 --- a/kuma/wiki/utils.py +++ b/kuma/wiki/utils.py @@ -1,5 +1,3 @@ - - import datetime import json from urllib.parse import urlparse @@ -24,13 +22,13 @@ def locale_and_slug_from_path(path, request=None, path_locale=None): locale or even a modern Kuma domain in the path. If so, signal for a redirect to a more canonical path. In any case, produce a locale and slug derived from the given path.""" - locale, slug, needs_redirect = '', path, False + locale, slug, needs_redirect = "", path, False mdn_locales = {lang[0].lower(): lang[0] for lang in settings.LANGUAGES} # If there's a slash in the path, then the first segment could be a # locale. And, that locale could even be a legacy MindTouch locale. - if '/' in path: - maybe_locale, maybe_slug = path.split('/', 1) + if "/" in path: + maybe_locale, maybe_slug = path.split("/", 1) l_locale = maybe_locale.lower() if l_locale in settings.MT_TO_KUMA_LOCALE_MAP: @@ -46,15 +44,15 @@ def locale_and_slug_from_path(path, request=None, path_locale=None): slug = maybe_slug # No locale yet? Try the locale detected by the request or in path - if locale == '': + if locale == "": if request: locale = request.LANGUAGE_CODE elif path_locale: locale = path_locale # Still no locale? Probably no request. Go with the site default. - if locale == '': - locale = getattr(settings, 'WIKI_DEFAULT_LANGUAGE', 'en-US') + if locale == "": + locale = getattr(settings, "WIKI_DEFAULT_LANGUAGE", "en-US") return (locale, slug, needs_redirect) @@ -82,28 +80,29 @@ def get_doc_components_from_url(url, required_locale=None, check_host=True): return False # View imports Model, Model imports utils, utils import Views. - from kuma.wiki.views.document import (document as document_view, - react_document as react_document_view) + from kuma.wiki.views.document import ( + document as document_view, + react_document as react_document_view, + ) if view not in (document_view, react_document_view): raise NotDocumentView - path = '/' + path - return locale, path, view_kwargs['document_path'] + path = "/" + path + return locale, path, view_kwargs["document_path"] def tidy_content(content): options = { - 'output-xhtml': 0, - 'force-output': 1, + "output-xhtml": 0, + "force-output": 1, } try: content = tidylib.tidy_document(content, options=options) except UnicodeDecodeError: # In case something happens in pytidylib we'll try again with # a proper encoding - content = tidylib.tidy_document(content.encode(), - options=options) + content = tidylib.tidy_document(content.encode(), options=options) tidied, errors = content return tidied.decode(), errors else: @@ -117,63 +116,69 @@ def analytics_upageviews(revision_ids, start_date, end_date=None): """ - scopes = ['https://www.googleapis.com/auth/analytics.readonly'] + scopes = ["https://www.googleapis.com/auth/analytics.readonly"] try: ga_cred_dict = json.loads(config.GOOGLE_ANALYTICS_CREDENTIALS) except (ValueError, TypeError): raise ImproperlyConfigured( - "GOOGLE_ANALYTICS_CREDENTIALS Constance setting is badly formed.") + "GOOGLE_ANALYTICS_CREDENTIALS Constance setting is badly formed." + ) if not ga_cred_dict: raise ImproperlyConfigured( - "An empty GOOGLE_ANALYTICS_CREDENTIALS Constance setting is not permitted.") + "An empty GOOGLE_ANALYTICS_CREDENTIALS Constance setting is not permitted." + ) - credentials = ServiceAccountCredentials.from_json_keyfile_dict(ga_cred_dict, - scopes=scopes) + credentials = ServiceAccountCredentials.from_json_keyfile_dict( + ga_cred_dict, scopes=scopes + ) http_auth = credentials.authorize(Http()) - service = build('analyticsreporting', 'v4', http=http_auth) + service = build("analyticsreporting", "v4", http=http_auth) if end_date is None: end_date = datetime.date.today() - if hasattr(start_date, 'date'): + if hasattr(start_date, "date"): start_date = start_date.date() - if hasattr(end_date, 'date'): + if hasattr(end_date, "date"): end_date = end_date.date() start_date = start_date.isoformat() end_date = end_date.isoformat() request = service.reports().batchGet( body={ - 'reportRequests': [ + "reportRequests": [ # `dimension12` is the custom variable containing a page's rev #. { - 'dimensions': [{'name': 'ga:dimension12'}], - 'metrics': [{'expression': 'ga:uniquePageviews'}], - 'dimensionFilterClauses': [ + "dimensions": [{"name": "ga:dimension12"}], + "metrics": [{"expression": "ga:uniquePageviews"}], + "dimensionFilterClauses": [ { - 'filters': [ - {'dimensionName': 'ga:dimension12', - 'operator': 'IN_LIST', - 'expressions': [str(x) for x in revision_ids]} + "filters": [ + { + "dimensionName": "ga:dimension12", + "operator": "IN_LIST", + "expressions": [str(x) for x in revision_ids], + } ] } ], - 'dateRanges': [ - {'startDate': start_date, 'endDate': end_date} - ], - 'viewId': '66726481' # PK of the developer.mozilla.org site on GA. + "dateRanges": [{"startDate": start_date, "endDate": end_date}], + "viewId": "66726481", # PK of the developer.mozilla.org site on GA. } ] - }) + } + ) response = request.execute() data = {int(r): 0 for r in revision_ids} - data.update({ - int(row['dimensions'][0]): int(row['metrics'][0]['values'][0]) - for row in response['reports'][0]['data'].get('rows', ()) - }) + data.update( + { + int(row["dimensions"][0]): int(row["metrics"][0]["values"][0]) + for row in response["reports"][0]["data"].get("rows", ()) + } + ) return data diff --git a/kuma/wiki/views/__init__.py b/kuma/wiki/views/__init__.py index 2e966992c9a..b083a1276b9 100644 --- a/kuma/wiki/views/__init__.py +++ b/kuma/wiki/views/__init__.py @@ -1,8 +1,17 @@ - - import logging -from . import (akismet_revision, code, create, delete, document, edit, # noqa - legacy, list, misc, revision, translate) # noqa +from . import ( # noqa: F401 + akismet_revision, + code, + create, + delete, + document, + edit, + legacy, + list, + misc, + revision, + translate, +) -log = logging.getLogger('kuma.wiki.views') +log = logging.getLogger("kuma.wiki.views") diff --git a/kuma/wiki/views/akismet_revision.py b/kuma/wiki/views/akismet_revision.py index 740939f6a50..197aa852ce9 100644 --- a/kuma/wiki/views/akismet_revision.py +++ b/kuma/wiki/views/akismet_revision.py @@ -1,5 +1,3 @@ - - import json from django.contrib.auth.decorators import permission_required @@ -19,7 +17,7 @@ @never_cache @csrf_exempt @require_POST -@permission_required('wiki.add_revisionakismetsubmission') +@permission_required("wiki.add_revisionakismetsubmission") def submit_akismet_spam(request): """ Creates SPAM Akismet record for revision. @@ -29,25 +27,35 @@ def submit_akismet_spam(request): """ submission = RevisionAkismetSubmission(sender=request.user, type="spam") - data = RevisionAkismetSubmissionSpamForm(data=request.POST, instance=submission, request=request) + data = RevisionAkismetSubmissionSpamForm( + data=request.POST, instance=submission, request=request + ) if data.is_valid(): data.save() - revision = data.cleaned_data['revision'] - akismet_revisions = (RevisionAkismetSubmission.objects.filter(revision=revision) - .order_by('id') - .values('sender__username', 'sent', 'type')) + revision = data.cleaned_data["revision"] + akismet_revisions = ( + RevisionAkismetSubmission.objects.filter(revision=revision) + .order_by("id") + .values("sender__username", "sent", "type") + ) data = [ { "sender": rev["sender__username"], - "sent": format_date_time(value=rev["sent"], - format='datetime', request=request)[0], - "type": rev["type"]} - for rev in akismet_revisions] - - return HttpResponse(json.dumps(data, sort_keys=True), - content_type='application/json; charset=utf-8', status=201) + "sent": format_date_time( + value=rev["sent"], format="datetime", request=request + )[0], + "type": rev["type"], + } + for rev in akismet_revisions + ] + + return HttpResponse( + json.dumps(data, sort_keys=True), + content_type="application/json; charset=utf-8", + status=201, + ) return HttpResponseBadRequest() diff --git a/kuma/wiki/views/code.py b/kuma/wiki/views/code.py index d167af56b0b..823c146737a 100644 --- a/kuma/wiki/views/code.py +++ b/kuma/wiki/views/code.py @@ -1,5 +1,3 @@ - - from django.conf import settings from django.core.exceptions import PermissionDenied from django.shortcuts import get_object_or_404, redirect, render @@ -25,17 +23,15 @@ def code_sample(request, document_slug, document_locale, sample_name): HTML document """ # Restrict rendering of live code samples to specified hosts - if request.get_host() not in (settings.ATTACHMENT_HOST, - settings.ATTACHMENT_ORIGIN): + if request.get_host() not in (settings.ATTACHMENT_HOST, settings.ATTACHMENT_ORIGIN): raise PermissionDenied - document = get_object_or_404(Document, slug=document_slug, - locale=document_locale) + document = get_object_or_404(Document, slug=document_slug, locale=document_locale) job = DocumentCodeSampleJob(generation_args=[document.pk]) data = job.get(document.pk, sample_name) - data['document'] = document - data['sample_name'] = sample_name - return render(request, 'wiki/code_sample.html', data) + data["document"] = document + data["sample_name"] = sample_name + return render(request, "wiki/code_sample.html", data) @cache_control(public=True, max_age=60 * 60 * 24 * 5) @@ -43,8 +39,9 @@ def code_sample(request, document_slug, document_locale, sample_name): @allow_CORS_GET @xframe_options_exempt @process_document_path -def raw_code_sample_file(request, document_slug, document_locale, - sample_name, attachment_id, filename): +def raw_code_sample_file( + request, document_slug, document_locale, sample_name, attachment_id, filename +): """ A view redirecting to the real file serving view of the attachments app. This exists so the writers can use relative paths to files in the diff --git a/kuma/wiki/views/create.py b/kuma/wiki/views/create.py index fc41d0d3bfb..55834eb6598 100644 --- a/kuma/wiki/views/create.py +++ b/kuma/wiki/views/create.py @@ -1,13 +1,10 @@ - - import newrelic.agent from constance import config from django.shortcuts import redirect, render from django.views.decorators.cache import never_cache from kuma.attachments.forms import AttachmentRevisionForm -from kuma.core.decorators import (block_user_agents, ensure_wiki_domain, - login_required) +from kuma.core.decorators import block_user_agents, ensure_wiki_domain, login_required from kuma.core.urlresolvers import reverse from ..constants import DEV_DOC_REQUEST_FORM, REVIEW_FLAG_TAGS_DEFAULT @@ -27,48 +24,47 @@ def create(request): """ Create a new wiki page, which is a document and a revision. """ - initial_slug = request.GET.get('slug', '') + initial_slug = request.GET.get("slug", "") # TODO: Integrate this into a new exception-handling middleware - if not request.user.has_perm('wiki.add_document'): + if not request.user.has_perm("wiki.add_document"): context = { - 'reason': 'create-page', - 'request_page_url': DEV_DOC_REQUEST_FORM, - 'email_address': config.EMAIL_LIST_MDN_ADMINS + "reason": "create-page", + "request_page_url": DEV_DOC_REQUEST_FORM, + "email_address": config.EMAIL_LIST_MDN_ADMINS, } - return render(request, '403-create-page.html', context=context, - status=403) + return render(request, "403-create-page.html", context=context, status=403) # a fake title based on the initial slug passed via a query parameter - initial_title = initial_slug.replace('_', ' ') + initial_title = initial_slug.replace("_", " ") # in case we want to create a sub page under a different document try: # If a parent ID is provided via GET, confirm it exists - initial_parent_id = int(request.GET.get('parent', '')) + initial_parent_id = int(request.GET.get("parent", "")) parent_doc = Document.objects.get(pk=initial_parent_id) parent_slug = parent_doc.slug parent_path = parent_doc.get_absolute_url() except (ValueError, Document.DoesNotExist): - initial_parent_id = parent_slug = parent_path = '' + initial_parent_id = parent_slug = parent_path = "" # in case we want to create a new page by cloning an existing document try: - clone_id = int(request.GET.get('clone', '')) + clone_id = int(request.GET.get("clone", "")) except ValueError: clone_id = None context = { - 'attachment_form': AttachmentRevisionForm(), - 'parent_path': parent_path, - 'parent_slug': parent_slug, + "attachment_form": AttachmentRevisionForm(), + "parent_path": parent_path, + "parent_slug": parent_slug, } - if request.method == 'GET': + if request.method == "GET": initial_data = {} - initial_html = '' - initial_tags = '' + initial_html = "" + initial_tags = "" initial_toc = Revision.TOC_DEPTH_H4 if clone_id: @@ -86,60 +82,59 @@ def create(request): pass if parent_slug: - initial_data['parent_topic'] = initial_parent_id + initial_data["parent_topic"] = initial_parent_id if initial_slug: - initial_data['title'] = initial_title - initial_data['slug'] = initial_slug + initial_data["title"] = initial_title + initial_data["slug"] = initial_slug review_tags = REVIEW_FLAG_TAGS_DEFAULT doc_form = DocumentForm(initial=initial_data, parent_slug=parent_slug) initial = { - 'slug': initial_slug, - 'title': initial_title, - 'content': initial_html, - 'review_tags': review_tags, - 'tags': initial_tags, - 'toc_depth': initial_toc + "slug": initial_slug, + "title": initial_title, + "content": initial_html, + "review_tags": review_tags, + "tags": initial_tags, + "toc_depth": initial_toc, } rev_form = RevisionForm(request=request, initial=initial) - context.update({ - 'parent_id': initial_parent_id, - 'document_form': doc_form, - 'revision_form': rev_form, - 'initial_tags': initial_tags, - }) + context.update( + { + "parent_id": initial_parent_id, + "document_form": doc_form, + "revision_form": rev_form, + "initial_tags": initial_tags, + } + ) else: submitted_data = request.POST.copy() - posted_slug = submitted_data['slug'] - submitted_data['locale'] = request.LANGUAGE_CODE + posted_slug = submitted_data["slug"] + submitted_data["locale"] = request.LANGUAGE_CODE if parent_slug: - submitted_data['parent_topic'] = initial_parent_id + submitted_data["parent_topic"] = initial_parent_id doc_form = DocumentForm(data=submitted_data, parent_slug=parent_slug) - rev_form = RevisionForm(request=request, - data=submitted_data, - parent_slug=parent_slug) + rev_form = RevisionForm( + request=request, data=submitted_data, parent_slug=parent_slug + ) if doc_form.is_valid() and rev_form.is_valid(): doc = doc_form.save(parent=None) rev_form.save(doc) if doc.current_revision.is_approved: - view = 'wiki.document' + view = "wiki.document" else: - view = 'wiki.document_revisions' + view = "wiki.document_revisions" return redirect(reverse(view, args=[doc.slug])) else: - doc_form.data['slug'] = posted_slug + doc_form.data["slug"] = posted_slug - context.update({ - 'document_form': doc_form, - 'revision_form': rev_form, - }) + context.update({"document_form": doc_form, "revision_form": rev_form}) - return render(request, 'wiki/create.html', context) + return render(request, "wiki/create.html", context) diff --git a/kuma/wiki/views/delete.py b/kuma/wiki/views/delete.py index 80fe66bd8bb..cd80d192a5a 100644 --- a/kuma/wiki/views/delete.py +++ b/kuma/wiki/views/delete.py @@ -1,12 +1,14 @@ - - from django.db import IntegrityError from django.shortcuts import get_object_or_404, redirect, render from django.utils.translation import ugettext from django.views.decorators.cache import never_cache -from kuma.core.decorators import (block_user_agents, ensure_wiki_domain, - login_required, permission_required) +from kuma.core.decorators import ( + block_user_agents, + ensure_wiki_domain, + login_required, + permission_required, +) from kuma.core.urlresolvers import reverse from ..decorators import check_readonly, process_document_path @@ -24,56 +26,61 @@ def revert_document(request, document_path, revision_id): """ Revert document to a specific revision. """ - document_locale, document_slug, needs_redirect = ( - locale_and_slug_from_path(document_path, request)) + document_locale, document_slug, needs_redirect = locale_and_slug_from_path( + document_path, request + ) - revision = get_object_or_404(Revision.objects.select_related('document'), - pk=revision_id, - document__slug=document_slug) + revision = get_object_or_404( + Revision.objects.select_related("document"), + pk=revision_id, + document__slug=document_slug, + ) - if request.method == 'GET': + if request.method == "GET": # Render the confirmation page - return render(request, 'wiki/confirm_revision_revert.html', - {'revision': revision, 'document': revision.document}) + return render( + request, + "wiki/confirm_revision_revert.html", + {"revision": revision, "document": revision.document}, + ) else: - comment = request.POST.get('comment') + comment = request.POST.get("comment") document = revision.document old_revision_pk = revision.pk try: new_revision = document.revert(revision, request.user, comment) # schedule a rendering of the new revision if it really was saved if new_revision.pk != old_revision_pk: - document.schedule_rendering('max-age=0') + document.schedule_rendering("max-age=0") except IntegrityError: return render( request, - 'wiki/confirm_revision_revert.html', + "wiki/confirm_revision_revert.html", { - 'revision': revision, - 'document': revision.document, - 'error': ugettext( + "revision": revision, + "document": revision.document, + "error": ugettext( "Document already exists. Note: You cannot " "revert a document that has been moved until you " - "delete its redirect.") - } + "delete its redirect." + ), + }, ) - return redirect('wiki.document_revisions', revision.document.slug) + return redirect("wiki.document_revisions", revision.document.slug) @ensure_wiki_domain @never_cache @block_user_agents @login_required -@permission_required('wiki.delete_document') +@permission_required("wiki.delete_document") @check_readonly @process_document_path def delete_document(request, document_slug, document_locale): """ Delete a Document. """ - document = get_object_or_404(Document, - locale=document_locale, - slug=document_slug) + document = get_object_or_404(Document, locale=document_locale, slug=document_slug) # HACK: https://bugzil.la/972545 - Don't delete pages that have children # TODO: https://bugzil.la/972541 - Deleting a page that has subpages @@ -81,14 +88,14 @@ def delete_document(request, document_slug, document_locale): first_revision = document.revisions.all()[0] - if request.method == 'POST': + if request.method == "POST": form = DocumentDeletionForm(data=request.POST) if form.is_valid(): DocumentDeletionLog.objects.create( locale=document.locale, slug=document.slug, user=request.user, - reason=form.cleaned_data['reason'] + reason=form.cleaned_data["reason"], ) document.delete() return redirect(document) @@ -96,29 +103,29 @@ def delete_document(request, document_slug, document_locale): form = DocumentDeletionForm() context = { - 'document': document, - 'form': form, - 'request': request, - 'revision': first_revision, - 'prevent': prevent, + "document": document, + "form": form, + "request": request, + "revision": first_revision, + "prevent": prevent, } - return render(request, 'wiki/confirm_document_delete.html', context) + return render(request, "wiki/confirm_document_delete.html", context) @ensure_wiki_domain @never_cache @block_user_agents @login_required -@permission_required('wiki.restore_document') +@permission_required("wiki.restore_document") @check_readonly @process_document_path def restore_document(request, document_slug, document_locale): """ Restore a deleted Document. """ - document = get_object_or_404(Document.deleted_objects.all(), - slug=document_slug, - locale=document_locale) + document = get_object_or_404( + Document.deleted_objects.all(), slug=document_slug, locale=document_locale + ) document.restore() return redirect(document) @@ -127,33 +134,32 @@ def restore_document(request, document_slug, document_locale): @never_cache @block_user_agents @login_required -@permission_required('wiki.purge_document') +@permission_required("wiki.purge_document") @check_readonly @process_document_path def purge_document(request, document_slug, document_locale): """ Permanently purge a deleted Document. """ - document = get_object_or_404(Document.deleted_objects.all(), - slug=document_slug, - locale=document_locale) + document = get_object_or_404( + Document.deleted_objects.all(), slug=document_slug, locale=document_locale + ) deletion_log_entries = DocumentDeletionLog.objects.filter( - locale=document_locale, - slug=document_slug + locale=document_locale, slug=document_slug ) if deletion_log_entries.exists(): - deletion_log = deletion_log_entries.order_by('-pk')[0] + deletion_log = deletion_log_entries.order_by("-pk")[0] else: deletion_log = {} - if request.method == 'POST' and 'confirm' in request.POST: + if request.method == "POST" and "confirm" in request.POST: document.purge() - return redirect(reverse('wiki.document', - args=(document_slug,), - locale=document_locale)) + return redirect( + reverse("wiki.document", args=(document_slug,), locale=document_locale) + ) else: - return render(request, - 'wiki/confirm_purge.html', - {'document': document, - 'deletion_log': deletion_log, - }) + return render( + request, + "wiki/confirm_purge.html", + {"document": document, "deletion_log": deletion_log}, + ) diff --git a/kuma/wiki/views/document.py b/kuma/wiki/views/document.py index 05951d33114..7016ec7cbe6 100644 --- a/kuma/wiki/views/document.py +++ b/kuma/wiki/views/document.py @@ -1,5 +1,3 @@ - - import json from io import BytesIO @@ -7,8 +5,13 @@ from django.conf import settings from django.contrib import messages from django.core.exceptions import PermissionDenied -from django.http import (Http404, HttpResponse, HttpResponseBadRequest, - HttpResponsePermanentRedirect, JsonResponse) +from django.http import ( + Http404, + HttpResponse, + HttpResponseBadRequest, + HttpResponsePermanentRedirect, + JsonResponse, +) from django.http.multipartparser import MultiPartParser from django.shortcuts import get_object_or_404, redirect, render from django.utils.cache import add_never_cache_headers, patch_vary_headers @@ -17,21 +20,22 @@ from django.utils.translation import ugettext from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_exempt -from django.views.decorators.http import (require_GET, require_http_methods, - require_POST) +from django.views.decorators.http import require_GET, require_http_methods, require_POST from pyquery import PyQuery as pq from ratelimit.decorators import ratelimit import kuma.wiki.content from kuma.api.v1.views import document_api_data from kuma.authkeys.decorators import accepts_auth_key -from kuma.core.decorators import (block_user_agents, - ensure_wiki_domain, - login_required, - permission_required, - redirect_in_maintenance_mode, - shared_cache_control, - superuser_required) +from kuma.core.decorators import ( + block_user_agents, + ensure_wiki_domain, + login_required, + permission_required, + redirect_in_maintenance_mode, + shared_cache_control, + superuser_required, +) from kuma.core.urlresolvers import reverse from kuma.core.utils import is_wiki, redirect_to_wiki, to_html, urlparams from kuma.search.store import get_search_url_from_referer @@ -39,12 +43,15 @@ from .utils import calculate_etag, split_slug from .. import kumascript from ..constants import SLUG_CLEANSING_RE, WIKI_ONLY_DOCUMENT_QUERY_PARAMS -from ..decorators import (allow_CORS_GET, check_readonly, prevent_indexing, - process_document_path) +from ..decorators import ( + allow_CORS_GET, + check_readonly, + prevent_indexing, + process_document_path, +) from ..events import EditDocumentEvent, EditDocumentInTreeEvent from ..forms import TreeMoveForm -from ..models import (Document, DocumentDeletionLog, - DocumentRenderedContentNotAvailable) +from ..models import Document, DocumentDeletionLog, DocumentRenderedContentNotAvailable from ..tasks import move_page @@ -61,18 +68,18 @@ def _get_html_and_errors(request, doc, rendering_params): attempted. If False, pre-rendered content is returned, if any. """ doc_html, ks_errors, render_raw_fallback = doc.html, None, False - if not rendering_params['use_rendered']: + if not rendering_params["use_rendered"]: return doc_html, ks_errors, render_raw_fallback # A logged-in user can schedule a full re-render with Shift-Reload cache_control = None if request.user.is_authenticated: # Shift-Reload sends Cache-Control: no-cache - ua_cc = request.META.get('HTTP_CACHE_CONTROL') - if ua_cc == 'no-cache': - cache_control = 'no-cache' + ua_cc = request.META.get("HTTP_CACHE_CONTROL") + if ua_cc == "no-cache": + cache_control = "no-cache" - base_url = request.build_absolute_uri('/') + base_url = request.build_absolute_uri("/") try: r_body, r_errors = doc.get_rendered(cache_control, base_url) if r_body: @@ -94,14 +101,14 @@ def _make_doc_structure(document, level, expand, depth): if expand: result = dict(document.get_json_data()) - result['subpages'] = [] + result["subpages"] = [] else: result = { - 'title': document.title, - 'slug': document.slug, - 'locale': document.locale, - 'url': document.get_absolute_url(), - 'subpages': [] + "title": document.title, + "slug": document.slug, + "locale": document.locale, + "url": document.get_absolute_url(), + "subpages": [], } if level < depth: @@ -110,7 +117,7 @@ def _make_doc_structure(document, level, expand, depth): for descendant in descendants: subpage = _make_doc_structure(descendant, level + 1, expand, depth) if subpage is not None: - result['subpages'].append(subpage) + result["subpages"].append(subpage) return result @@ -118,7 +125,7 @@ def _get_seo_parent_title(document, slug_dict, document_locale): """ Get parent-title information for SEO purposes. """ - seo_doc_slug = slug_dict['seo_root'] + seo_doc_slug = slug_dict["seo_root"] seo_root_doc = None if seo_doc_slug: @@ -127,14 +134,16 @@ def _get_seo_parent_title(document, slug_dict, document_locale): seo_root_doc = document.parent_topic else: try: - seo_root_doc = Document.objects.only('title').get(locale=document_locale, slug=seo_doc_slug) + seo_root_doc = Document.objects.only("title").get( + locale=document_locale, slug=seo_doc_slug + ) except Document.DoesNotExist: pass if seo_root_doc: - return ' - {}'.format(seo_root_doc.title) + return " - {}".format(seo_root_doc.title) else: - return '' + return "" def _filter_doc_html(request, doc, doc_html, rendering_params): @@ -142,13 +151,17 @@ def _filter_doc_html(request, doc, doc_html, rendering_params): Apply needed filtering/annotating operations to a Document's HTML. """ # If ?summary is on, just serve up the summary as doc HTML - if rendering_params['summary']: + if rendering_params["summary"]: return doc.get_summary_html() # Shortcut the parsing & filtering, if none of these relevant rendering # params are set. - if not (rendering_params['section'] or rendering_params['raw'] or - rendering_params['edit_links'] or rendering_params['include']): + if not ( + rendering_params["section"] + or rendering_params["raw"] + or rendering_params["edit_links"] + or rendering_params["include"] + ): return doc_html # TODO: One more view-time content parsing instance to refactor @@ -156,7 +169,7 @@ def _filter_doc_html(request, doc, doc_html, rendering_params): # ?raw view is often used for editors - apply safety filtering. # TODO: Should this stuff happen in render() itself? - if rendering_params['raw']: + if rendering_params["raw"]: # TODO: There will be no need to call "injectSectionIDs" or # "filterEditorSafety" when the code that calls "clean_content" # on Revision.save is deployed to production, AND the current @@ -167,13 +180,14 @@ def _filter_doc_html(request, doc, doc_html, rendering_params): # If a section ID is specified, extract that section. # TODO: Pre-extract every section on render? Might be over-optimization - if rendering_params['section']: - tool.extractSection(rendering_params['section']) + if rendering_params["section"]: + tool.extractSection(rendering_params["section"]) # If this user can edit the document, inject section editing links. # TODO: Rework so that this happens on the client side? - if ((rendering_params['edit_links'] or not rendering_params['raw']) and - request.user.is_authenticated): + if ( + rendering_params["edit_links"] or not rendering_params["raw"] + ) and request.user.is_authenticated: tool.injectSectionEditingLinks(doc.slug, doc.locale) doc_html = tool.serialize() @@ -182,7 +196,7 @@ def _filter_doc_html(request, doc, doc_html, rendering_params): # TODO: Any way to make this work in rendering? Possibly over-optimization, # because this is often paired with ?section - so we'd need to store every # section twice for with & without include sections - if rendering_params['include']: + if rendering_params["include"]: doc_html = kuma.wiki.content.filter_out_noinclude(doc_html) return doc_html @@ -193,7 +207,7 @@ def _add_kuma_revision_header(doc, response): Add the X-kuma-revision header to the response if applicable. """ if doc.current_revision_id: - response['X-kuma-revision'] = doc.current_revision_id + response["X-kuma-revision"] = doc.current_revision_id return response @@ -210,8 +224,7 @@ def _default_locale_fallback(request, document_slug, document_locale): try: fallback_doc = Document.objects.get( - slug=document_slug, - locale=settings.WIKI_DEFAULT_LANGUAGE + slug=document_slug, locale=settings.WIKI_DEFAULT_LANGUAGE ) # If there's a translation to the requested locale, take it: @@ -223,11 +236,11 @@ def _default_locale_fallback(request, document_slug, document_locale): elif translation and fallback_doc.current_revision: # Found a translation but its current_revision is None # and OK to fall back to parent (parent is approved). - fallback_reason = 'translation_not_approved' + fallback_reason = "translation_not_approved" elif fallback_doc.current_revision: # There is no translation # and OK to fall back to parent (parent is approved). - fallback_reason = 'no_translation' + fallback_reason = "no_translation" except Document.DoesNotExist: pass @@ -244,30 +257,63 @@ def _get_doc_and_fallback_reason(document_locale, document_slug): fallback_reason = None # Optimizing the queryset to fetch the required values only - related_fields = ['current_revision', 'current_revision__creator', - 'parent', 'parent__current_revision', 'parent_topic'] - current_revision_fields = ['current_revision__{}'.format(field) for field in - ('toc_depth', 'created', 'creator__id', 'creator__username', 'creator__is_active')] - parent_fields = ['parent__{}'.format(field) for field in ('locale', 'slug', 'current_revision__slug')] - parent_topic_fields = ['parent_topic__{}'.format(field) for field in ('id', 'title', 'slug')] - - document_fields = ['html', 'rendered_html', 'body_html', - 'locale', 'slug', 'title', 'is_localizable', 'rendered_errors', - 'toc_html', 'summary_html', 'summary_text', 'quick_links_html'] - - fields = document_fields + current_revision_fields + parent_fields + parent_topic_fields + related_fields = [ + "current_revision", + "current_revision__creator", + "parent", + "parent__current_revision", + "parent_topic", + ] + current_revision_fields = [ + "current_revision__{}".format(field) + for field in ( + "toc_depth", + "created", + "creator__id", + "creator__username", + "creator__is_active", + ) + ] + parent_fields = [ + "parent__{}".format(field) + for field in ("locale", "slug", "current_revision__slug") + ] + parent_topic_fields = [ + "parent_topic__{}".format(field) for field in ("id", "title", "slug") + ] + + document_fields = [ + "html", + "rendered_html", + "body_html", + "locale", + "slug", + "title", + "is_localizable", + "rendered_errors", + "toc_html", + "summary_html", + "summary_text", + "quick_links_html", + ] + + fields = ( + document_fields + current_revision_fields + parent_fields + parent_topic_fields + ) try: - doc = (Document.objects.only(*fields).select_related(*related_fields) - .get(locale=document_locale, slug=document_slug)) + doc = ( + Document.objects.only(*fields) + .select_related(*related_fields) + .get(locale=document_locale, slug=document_slug) + ) - if (not doc.current_revision_id and doc.parent and - doc.parent.current_revision): + if not doc.current_revision_id and doc.parent and doc.parent.current_revision: # This is a translation but its current_revision is None # and OK to fall back to parent (parent is approved). - fallback_reason = 'translation_not_approved' + fallback_reason = "translation_not_approved" elif not doc.current_revision_id: - fallback_reason = 'no_content' + fallback_reason = "no_content" except Document.DoesNotExist: pass @@ -289,35 +335,35 @@ def _apply_content_experiment(request, doc): """ key = "%s:%s" % (doc.locale, doc.slug) for experiment in settings.CONTENT_EXPERIMENTS: - if key in experiment['pages']: + if key in experiment["pages"]: # This page is under a content experiment - variants = experiment['pages'][key] + variants = experiment["pages"][key] exp_params = { - 'id': experiment['id'], - 'ga_name': experiment['ga_name'], - 'param': experiment['param'], - 'original_path': request.path, - 'variants': variants, - 'selected': None, - 'selection_is_valid': None, + "id": experiment["id"], + "ga_name": experiment["ga_name"], + "param": experiment["param"], + "original_path": request.path, + "variants": variants, + "selected": None, + "selection_is_valid": None, } # Which variant was selected? - selected = request.GET.get(experiment['param']) + selected = request.GET.get(experiment["param"]) if selected: - exp_params['selection_is_valid'] = False + exp_params["selection_is_valid"] = False for variant, variant_slug in variants.items(): if selected == variant: try: content_doc = Document.objects.get( - locale=doc.locale, - slug=variant_slug) + locale=doc.locale, slug=variant_slug + ) except Document.DoesNotExist: pass else: # Valid variant selected - exp_params['selected'] = selected - exp_params['selection_is_valid'] = True + exp_params["selected"] = selected + exp_params["selection_is_valid"] = True return content_doc, exp_params return doc, exp_params # No (valid) variant selected return doc, None # Not a content experiment @@ -332,21 +378,20 @@ def children(request, document_slug, document_locale): """ Retrieves a document and returns its children in a JSON structure """ - expand = 'expand' in request.GET + expand = "expand" in request.GET max_depth = 5 - depth = int(request.GET.get('depth', max_depth)) + depth = int(request.GET.get("depth", max_depth)) if depth > max_depth: depth = max_depth result = [] try: - doc = Document.objects.get(locale=document_locale, - slug=document_slug) + doc = Document.objects.get(locale=document_locale, slug=document_slug) result = _make_doc_structure(doc, 0, expand, depth) if result is None: - result = {'error': 'Document has moved.'} + result = {"error": "Document has moved."} except Document.DoesNotExist: - result = {'error': 'Document does not exist.'} + result = {"error": "Document does not exist."} return JsonResponse(result) @@ -354,8 +399,8 @@ def children(request, document_slug, document_locale): @ensure_wiki_domain @never_cache @block_user_agents -@require_http_methods(['GET', 'POST']) -@permission_required('wiki.move_tree') +@require_http_methods(["GET", "POST"]) +@permission_required("wiki.move_tree") @process_document_path @check_readonly @prevent_indexing @@ -363,44 +408,52 @@ def move(request, document_slug, document_locale): """ Move a tree of pages """ - doc = get_object_or_404(Document, - locale=document_locale, - slug=document_slug) + doc = get_object_or_404(Document, locale=document_locale, slug=document_slug) descendants = doc.get_descendants() slug_split = split_slug(doc.slug) - if request.method == 'POST': + if request.method == "POST": form = TreeMoveForm(initial=request.GET, data=request.POST) if form.is_valid(): - conflicts = doc._tree_conflicts(form.cleaned_data['slug']) + conflicts = doc._tree_conflicts(form.cleaned_data["slug"]) if conflicts: - return render(request, 'wiki/move.html', { - 'form': form, - 'document': doc, - 'descendants': descendants, - 'descendants_count': len(descendants), - 'conflicts': conflicts, - 'SLUG_CLEANSING_RE': SLUG_CLEANSING_RE, - }) - move_page.delay(document_locale, document_slug, - form.cleaned_data['slug'], - request.user.id) - return render(request, 'wiki/move_requested.html', { - 'form': form, - 'document': doc - }) + return render( + request, + "wiki/move.html", + { + "form": form, + "document": doc, + "descendants": descendants, + "descendants_count": len(descendants), + "conflicts": conflicts, + "SLUG_CLEANSING_RE": SLUG_CLEANSING_RE, + }, + ) + move_page.delay( + document_locale, + document_slug, + form.cleaned_data["slug"], + request.user.id, + ) + return render( + request, "wiki/move_requested.html", {"form": form, "document": doc} + ) else: form = TreeMoveForm() - return render(request, 'wiki/move.html', { - 'form': form, - 'document': doc, - 'descendants': descendants, - 'descendants_count': len(descendants), - 'SLUG_CLEANSING_RE': SLUG_CLEANSING_RE, - 'specific_slug': slug_split['specific'] - }) + return render( + request, + "wiki/move.html", + { + "form": form, + "document": doc, + "descendants": descendants, + "descendants_count": len(descendants), + "SLUG_CLEANSING_RE": SLUG_CLEANSING_RE, + "specific_slug": slug_split["specific"], + }, + ) @ensure_wiki_domain @@ -410,9 +463,7 @@ def move(request, document_slug, document_locale): @superuser_required @check_readonly def repair_breadcrumbs(request, document_slug, document_locale): - doc = get_object_or_404(Document, - locale=document_locale, - slug=document_slug) + doc = get_object_or_404(Document, locale=document_locale, slug=document_slug) doc.repair_breadcrumbs() return redirect(doc.get_absolute_url()) @@ -423,29 +474,29 @@ def repair_breadcrumbs(request, document_slug, document_locale): @allow_CORS_GET @process_document_path @prevent_indexing -@ratelimit(key='user_or_ip', rate='400/m', block=True) +@ratelimit(key="user_or_ip", rate="400/m", block=True) def toc(request, document_slug=None, document_locale=None): """ Return a document's table of contents as HTML. """ query = { - 'locale': request.LANGUAGE_CODE, - 'current_revision__isnull': False, + "locale": request.LANGUAGE_CODE, + "current_revision__isnull": False, } if document_slug is not None: - query['slug'] = document_slug - query['locale'] = document_locale - elif 'title' in request.GET: - query['title'] = request.GET['title'] - elif 'slug' in request.GET: - query['slug'] = request.GET['slug'] + query["slug"] = document_slug + query["locale"] = document_locale + elif "title" in request.GET: + query["title"] = request.GET["title"] + elif "slug" in request.GET: + query["slug"] = request.GET["slug"] else: return HttpResponseBadRequest() document = get_object_or_404(Document, **query) toc_html = document.get_toc_html() if toc_html: - toc_html = '
      ' + toc_html + '
    ' + toc_html = "
      " + toc_html + "
    " return HttpResponse(toc_html) @@ -461,16 +512,16 @@ def as_json(request, document_slug=None, document_locale=None): Return some basic document info in a JSON blob. """ kwargs = { - 'locale': request.LANGUAGE_CODE, - 'current_revision__isnull': False, + "locale": request.LANGUAGE_CODE, + "current_revision__isnull": False, } if document_slug is not None: - kwargs['slug'] = document_slug - kwargs['locale'] = document_locale - elif 'title' in request.GET: - kwargs['title'] = request.GET['title'] - elif 'slug' in request.GET: - kwargs['slug'] = request.GET['slug'] + kwargs["slug"] = document_slug + kwargs["locale"] = document_locale + elif "title" in request.GET: + kwargs["title"] = request.GET["title"] + elif "slug" in request.GET: + kwargs["slug"] = request.GET["slug"] else: return HttpResponseBadRequest() @@ -479,16 +530,14 @@ def as_json(request, document_slug=None, document_locale=None): # code that calls "clean_content" on Revision.save is deployed to # production, AND the current revisions of all docs have had their # content cleaned with "clean_content". - (kuma.wiki.content.parse(document.html) - .injectSectionIDs() - .serialize()) + (kuma.wiki.content.parse(document.html).injectSectionIDs().serialize()) stale = True if is_wiki(request) and request.user.is_authenticated: # From the Wiki domain, a logged-in user can demand fresh data with # a shift-reload (which sends "Cache-Control: no-cache"). - ua_cc = request.META.get('HTTP_CACHE_CONTROL') - if ua_cc == 'no-cache': + ua_cc = request.META.get("HTTP_CACHE_CONTROL") + if ua_cc == "no-cache": stale = False data = document.get_json_data(stale=stale) @@ -506,8 +555,7 @@ def subscribe(request, document_slug, document_locale): """ Toggle watching a document for edits. """ - document = get_object_or_404( - Document, locale=document_locale, slug=document_slug) + document = get_object_or_404(Document, locale=document_locale, slug=document_slug) status = 0 if EditDocumentEvent.is_notifying(request.user, document): @@ -517,7 +565,7 @@ def subscribe(request, document_slug, document_locale): status = 1 if request.is_ajax(): - return JsonResponse({'status': status}) + return JsonResponse({"status": status}) else: return redirect(document) @@ -533,8 +581,7 @@ def subscribe_to_tree(request, document_slug, document_locale): """ Toggle watching a tree of documents for edits. """ - document = get_object_or_404( - Document, locale=document_locale, slug=document_slug) + document = get_object_or_404(Document, locale=document_locale, slug=document_slug) status = 0 if EditDocumentInTreeEvent.is_notifying(request.user, document): @@ -544,7 +591,7 @@ def subscribe_to_tree(request, document_slug, document_locale): status = 1 if request.is_ajax(): - return JsonResponse({'status': status}) + return JsonResponse({"status": status}) else: return redirect(document) @@ -554,19 +601,18 @@ def _document_redirect_to_create(document_slug, document_locale, slug_dict): When a Document doesn't exist but the user can create it, return the creation URL to redirect to. """ - url = reverse('wiki.create', locale=document_locale) - if slug_dict['length'] > 1: - parent_doc = get_object_or_404(Document, - locale=document_locale, - slug=slug_dict['parent']) + url = reverse("wiki.create", locale=document_locale) + if slug_dict["length"] > 1: + parent_doc = get_object_or_404( + Document, locale=document_locale, slug=slug_dict["parent"] + ) if parent_doc.is_redirect: parent_doc = parent_doc.get_redirect_document(id_only=True) if parent_doc is None: # Redirect is not to a Document, can't create subpage raise Http404() - url = urlparams(url, parent=parent_doc.id, - slug=slug_dict['specific']) + url = urlparams(url, parent=parent_doc.id, slug=slug_dict["specific"]) else: # This is a "base level" redirect, i.e. no parent url = urlparams(url, slug=document_slug) @@ -582,11 +628,10 @@ def _document_deleted(request, deletion_logs): If the user can restore documents, then return a 404 but also include the template with the form to restore the document. """ - if request.user and request.user.has_perm('wiki.restore_document'): - deletion_log = deletion_logs.order_by('-pk')[0] - context = {'deletion_log': deletion_log} - response = render(request, 'wiki/deletion_log.html', context, - status=404) + if request.user and request.user.has_perm("wiki.restore_document"): + deletion_log = deletion_logs.order_by("-pk")[0] + context = {"deletion_log": deletion_log} + response = render(request, "wiki/deletion_log.html", context, status=404) add_never_cache_headers(response) return response @@ -599,18 +644,18 @@ def _document_raw(doc_html): Display a raw Document. """ response = HttpResponse(doc_html) - response['X-Frame-Options'] = 'Allow' - response['X-Robots-Tag'] = 'noindex' + response["X-Frame-Options"] = "Allow" + response["X-Robots-Tag"] = "noindex" return response @shared_cache_control @csrf_exempt -@require_http_methods(['GET', 'HEAD']) +@require_http_methods(["GET", "HEAD"]) @allow_CORS_GET @process_document_path @newrelic.agent.function_trace() -@ratelimit(key='user_or_ip', rate='1200/m', block=True) +@ratelimit(key="user_or_ip", rate="1200/m", block=True) def document(request, document_slug, document_locale): if is_wiki(request): return wiki_document(request, document_slug, document_locale) @@ -625,31 +670,31 @@ def wiki_document(request, document_slug, document_locale): slug_dict = split_slug(document_slug) # Is there a document at this slug, in this locale? - doc, fallback_reason = _get_doc_and_fallback_reason(document_locale, - document_slug) + doc, fallback_reason = _get_doc_and_fallback_reason(document_locale, document_slug) if doc is None: # Possible the document once existed, but is now deleted. # If so, show that it was deleted. deletion_log_entries = DocumentDeletionLog.objects.filter( - locale=document_locale, - slug=document_slug + locale=document_locale, slug=document_slug ) if deletion_log_entries.exists(): # Show deletion log and restore / purge for soft-deleted docs deleted_doc = Document.deleted_objects.filter( - locale=document_locale, slug=document_slug) + locale=document_locale, slug=document_slug + ) if deleted_doc.exists(): return _document_deleted(request, deletion_log_entries) # We can throw a 404 immediately if the request type is HEAD. # TODO: take a shortcut if the document was found? - if request.method == 'HEAD': + if request.method == "HEAD": raise Http404 # Check if we should fall back to default locale. fallback_doc, fallback_reason, redirect_url = _default_locale_fallback( - request, document_slug, document_locale) + request, document_slug, document_locale + ) if fallback_doc is not None: doc = fallback_doc if redirect_url is not None: @@ -657,17 +702,23 @@ def wiki_document(request, document_slug, document_locale): else: # If a Document is not found, we may 404 immediately based on # request parameters. - if (any([request.GET.get(param, None) - for param in ('raw', 'include', 'nocreate')]) or - not request.user.is_authenticated): + if ( + any( + [ + request.GET.get(param, None) + for param in ("raw", "include", "nocreate") + ] + ) + or not request.user.is_authenticated + ): raise Http404 # The user may be trying to create a child page; if a parent exists # for this document, redirect them to the "Create" page # Otherwise, they could be trying to create a main level doc. - create_url = _document_redirect_to_create(document_slug, - document_locale, - slug_dict) + create_url = _document_redirect_to_create( + document_slug, document_locale, slug_dict + ) response = redirect(create_url) add_never_cache_headers(response) return response @@ -681,47 +732,52 @@ def wiki_document(request, document_slug, document_locale): # Don't redirect on redirect=no (like Wikipedia), so we can link from a # redirected-to-page back to a "Redirected from..." link, so you can edit # the redirect. - redirect_url = (None if request.GET.get('redirect') == 'no' - else doc.get_redirect_url()) + redirect_url = ( + None if request.GET.get("redirect") == "no" else doc.get_redirect_url() + ) if redirect_url and redirect_url != doc.get_absolute_url(): url = urlparams(redirect_url, query_dict=request.GET) # TODO: Re-enable the link in this message after Django >1.5 upgrade # Redirected from %(url)s messages.add_message( - request, messages.WARNING, - mark_safe(ugettext('Redirected from %(url)s') % { - "url": request.build_absolute_uri(doc.get_absolute_url()) - }), extra_tags='wiki_redirect') + request, + messages.WARNING, + mark_safe( + ugettext("Redirected from %(url)s") + % {"url": request.build_absolute_uri(doc.get_absolute_url())} + ), + extra_tags="wiki_redirect", + ) return HttpResponsePermanentRedirect(url) # Read some request params to see what we're supposed to do. rendering_params = {} - for param in ('raw', 'summary', 'include', 'edit_links'): + for param in ("raw", "summary", "include", "edit_links"): rendering_params[param] = request.GET.get(param, False) is not False - rendering_params['section'] = request.GET.get('section', None) - rendering_params['render_raw_fallback'] = False + rendering_params["section"] = request.GET.get("section", None) + rendering_params["render_raw_fallback"] = False # Are we in a content experiment? original_doc = doc doc, exp_params = _apply_content_experiment(request, doc) - rendering_params['experiment'] = exp_params + rendering_params["experiment"] = exp_params # Get us some HTML to play with. - rendering_params['use_rendered'] = ( - kumascript.should_use_rendered(doc, request.GET)) + rendering_params["use_rendered"] = kumascript.should_use_rendered(doc, request.GET) doc_html, ks_errors, render_raw_fallback = _get_html_and_errors( - request, doc, rendering_params) - rendering_params['render_raw_fallback'] = render_raw_fallback + request, doc, rendering_params + ) + rendering_params["render_raw_fallback"] = render_raw_fallback # Start parsing and applying filters. - if doc.show_toc and not rendering_params['raw']: + if doc.show_toc and not rendering_params["raw"]: toc_html = doc.get_toc_html() else: toc_html = None doc_html = _filter_doc_html(request, doc, doc_html, rendering_params) - if rendering_params['raw']: + if rendering_params["raw"]: response = _document_raw(doc_html) else: # Get the SEO summary @@ -729,7 +785,8 @@ def wiki_document(request, document_slug, document_locale): # Get the additional title information, if necessary. seo_parent_title = _get_seo_parent_title( - original_doc, slug_dict, document_locale) + original_doc, slug_dict, document_locale + ) # Retrieve pre-parsed content hunks quick_links_html = doc.get_quick_links_html() @@ -737,55 +794,57 @@ def wiki_document(request, document_slug, document_locale): # Record the English slug in Google Analytics, # to associate translations - if original_doc.locale == 'en-US': + if original_doc.locale == "en-US": en_slug = original_doc.slug - elif original_doc.parent_id and original_doc.parent.locale == 'en-US': + elif original_doc.parent_id and original_doc.parent.locale == "en-US": en_slug = original_doc.parent.slug else: - en_slug = '' + en_slug = "" - share_text = ugettext( - 'I learned about %(title)s on MDN.') % {"title": doc.title} + share_text = ugettext("I learned about %(title)s on MDN.") % { + "title": doc.title + } contributors = doc.contributors contributors_count = len(contributors) has_contributors = contributors_count > 0 other_translations = original_doc.get_other_translations( - fields=['title', 'locale', 'slug', 'parent'] + fields=["title", "locale", "slug", "parent"] + ) + all_locales = {original_doc.locale} | set( + trans.locale for trans in other_translations ) - all_locales = ({original_doc.locale} | - set(trans.locale for trans in other_translations)) # Bundle it all up and, finally, return. context = { - 'document': original_doc, - 'document_html': doc_html, - 'toc_html': toc_html, - 'quick_links_html': quick_links_html, - 'body_html': body_html, - 'contributors': contributors, - 'contributors_count': contributors_count, - 'contributors_limit': 6, - 'has_contributors': has_contributors, - 'fallback_reason': fallback_reason, - 'kumascript_errors': ks_errors, - 'macro_sources': ( + "document": original_doc, + "document_html": doc_html, + "toc_html": toc_html, + "quick_links_html": quick_links_html, + "body_html": body_html, + "contributors": contributors, + "contributors_count": contributors_count, + "contributors_limit": 6, + "has_contributors": has_contributors, + "fallback_reason": fallback_reason, + "kumascript_errors": ks_errors, + "macro_sources": ( kumascript.macro_sources(force_lowercase_keys=True) - if ks_errors else - None + if ks_errors + else None ), - 'render_raw_fallback': rendering_params['render_raw_fallback'], - 'seo_summary': seo_summary, - 'seo_parent_title': seo_parent_title, - 'share_text': share_text, - 'search_url': get_search_url_from_referer(request) or '', - 'analytics_page_revision': doc.current_revision_id, - 'analytics_en_slug': en_slug, - 'content_experiment': rendering_params['experiment'], - 'other_translations': other_translations, - 'all_locales': all_locales, + "render_raw_fallback": rendering_params["render_raw_fallback"], + "seo_summary": seo_summary, + "seo_parent_title": seo_parent_title, + "share_text": share_text, + "search_url": get_search_url_from_referer(request) or "", + "analytics_page_revision": doc.current_revision_id, + "analytics_en_slug": en_slug, + "content_experiment": rendering_params["experiment"], + "other_translations": other_translations, + "all_locales": all_locales, } - response = render(request, 'wiki/document.html', context) + response = render(request, "wiki/document.html", context) if ks_errors or request.user.is_authenticated: add_never_cache_headers(response) @@ -794,7 +853,7 @@ def wiki_document(request, document_slug, document_locale): # from erroneously caching without considering cookies, since cookies do # affect the content of the response. The primary CDN is configured to # cache based on a whitelist of cookies. - patch_vary_headers(response, ('Cookie',)) + patch_vary_headers(response, ("Cookie",)) return _add_kuma_revision_header(doc, response) @@ -811,19 +870,18 @@ def react_document(request, document_slug, document_locale): slug_dict = split_slug(document_slug) # Is there a document at this slug, in this locale? - doc, fallback_reason = _get_doc_and_fallback_reason( - document_locale, - document_slug) + doc, fallback_reason = _get_doc_and_fallback_reason(document_locale, document_slug) if doc is None: # We can throw a 404 immediately if the request type is HEAD. # TODO: take a shortcut if the document was found? - if request.method == 'HEAD': + if request.method == "HEAD": raise Http404 # Check if we should fall back to default locale. fallback_doc, fallback_reason, redirect_url = _default_locale_fallback( - request, document_slug, document_locale) + request, document_slug, document_locale + ) if fallback_doc is not None: doc = fallback_doc if redirect_url is not None: @@ -840,18 +898,23 @@ def react_document(request, document_slug, document_locale): # Don't redirect on redirect=no (like Wikipedia), so we can link from a # redirected-to-page back to a "Redirected from..." link, so you can edit # the redirect. - redirect_url = (None if request.GET.get('redirect') == 'no' - else doc.get_redirect_url()) + redirect_url = ( + None if request.GET.get("redirect") == "no" else doc.get_redirect_url() + ) if redirect_url and redirect_url != doc.get_absolute_url(): url = urlparams(redirect_url, query_dict=request.GET) # TODO: Re-enable the link in this message after Django >1.5 upgrade # Redirected from %(url)s messages.add_message( - request, messages.WARNING, - mark_safe(ugettext('Redirected from %(url)s') % { - "url": request.build_absolute_uri(doc.get_absolute_url()) - }), extra_tags='wiki_redirect') + request, + messages.WARNING, + mark_safe( + ugettext("Redirected from %(url)s") + % {"url": request.build_absolute_uri(doc.get_absolute_url())} + ), + extra_tags="wiki_redirect", + ) return HttpResponsePermanentRedirect(url) # Get the SEO summary @@ -862,7 +925,7 @@ def react_document(request, document_slug, document_locale): # Get the JSON data for this document doc_api_data = document_api_data(doc) - document_data = doc_api_data['documentData'] + document_data = doc_api_data["documentData"] def robots_index(): if fallback_reason: @@ -882,21 +945,18 @@ def robots_index(): return True - robots_meta_content = ( - 'index, follow' if robots_index() else 'noindex, nofollow' - ) + robots_meta_content = "index, follow" if robots_index() else "noindex, nofollow" # Bundle it all up and, finally, return. context = { - 'document_data': document_data, - + "document_data": document_data, # TODO: anything we're actually using in the template ought # to be bundled up into the json object above instead. - 'seo_summary': seo_summary, - 'seo_parent_title': seo_parent_title, - 'robots_meta_content': robots_meta_content, + "seo_summary": seo_summary, + "seo_parent_title": seo_parent_title, + "robots_meta_content": robots_meta_content, } - response = render(request, 'wiki/react_document.html', context) + response = render(request, "wiki/react_document.html", context) return _add_kuma_revision_header(doc, response) @@ -904,29 +964,25 @@ def robots_index(): @ensure_wiki_domain @shared_cache_control @csrf_exempt -@require_http_methods(['GET', 'HEAD', 'PUT']) -@redirect_in_maintenance_mode(methods=['PUT']) +@require_http_methods(["GET", "HEAD", "PUT"]) +@redirect_in_maintenance_mode(methods=["PUT"]) @allow_CORS_GET @accepts_auth_key @process_document_path @newrelic.agent.function_trace() -@ratelimit(key='user_or_ip', rate='100/m', block=True) +@ratelimit(key="user_or_ip", rate="100/m", block=True) def document_api(request, document_slug, document_locale): """ View/modify the content of a wiki document, or create a new wiki document. """ - if request.method == 'PUT': + if request.method == "PUT": if not (request.authkey and request.user.is_authenticated): raise PermissionDenied return _document_api_PUT(request, document_slug, document_locale) - doc = get_object_or_404( - Document, - slug=document_slug, - locale=document_locale - ) + doc = get_object_or_404(Document, slug=document_slug, locale=document_locale) - section_id = request.GET.get('section', None) + section_id = request.GET.get("section", None) response = HttpResponse(doc.get_html(section_id)) return _add_kuma_revision_header(doc, response) @@ -938,43 +994,43 @@ def _document_api_PUT(request, document_slug, document_locale): # Try parsing one of the supported content types from the request try: - content_type = request.META.get('CONTENT_TYPE', '') + content_type = request.META.get("CONTENT_TYPE", "") - if content_type.startswith('application/json'): + if content_type.startswith("application/json"): data = json.loads(request.body) - elif content_type.startswith('multipart/form-data'): - parser = MultiPartParser(request.META, - BytesIO(request.body), - request.upload_handlers, - request.encoding) + elif content_type.startswith("multipart/form-data"): + parser = MultiPartParser( + request.META, + BytesIO(request.body), + request.upload_handlers, + request.encoding, + ) data, _ = parser.parse() - elif content_type.startswith('text/html'): + elif content_type.startswith("text/html"): # TODO: Refactor this into wiki.content ? # First pass: Just assume the request body is an HTML fragment. - html = request.body.decode( - request.encoding or settings.DEFAULT_CHARSET) + html = request.body.decode(request.encoding or settings.DEFAULT_CHARSET) data = dict(content=html) # Second pass: Try parsing the body as a fuller HTML document, # and scrape out some of the interesting parts. try: doc = pq(html) - head_title = doc.find('head title') + head_title = doc.find("head title") if head_title.length > 0: - data['title'] = head_title.text() - body_content = doc.find('body') + data["title"] = head_title.text() + body_content = doc.find("body") if body_content.length > 0: - data['content'] = to_html(body_content) + data["content"] = to_html(body_content) except Exception: pass else: resp = HttpResponse() resp.status_code = 400 - resp.content = ugettext( - "Unsupported content-type: %s") % content_type + resp.content = ugettext("Unsupported content-type: %s") % content_type return resp except Exception as e: @@ -986,12 +1042,12 @@ def _document_api_PUT(request, document_slug, document_locale): try: # Look for existing document to edit: doc = Document.objects.get(locale=document_locale, slug=document_slug) - section_id = request.GET.get('section', None) + section_id = request.GET.get("section", None) is_new = False # Use ETags to detect mid-air edit collision # see: http://www.w3.org/1999/04/Editing/ - if_match = request.META.get('HTTP_IF_MATCH') + if_match = request.META.get("HTTP_IF_MATCH") if if_match: try: expected_etags = parse_etags(if_match) @@ -1003,7 +1059,7 @@ def _document_api_PUT(request, document_slug, document_locale): if current_etag not in expected_etags: resp = HttpResponse() resp.status_code = 412 - resp.content = ugettext('ETag precondition failed') + resp.content = ugettext("ETag precondition failed") return resp except Document.DoesNotExist: @@ -1011,31 +1067,36 @@ def _document_api_PUT(request, document_slug, document_locale): # Let's see if this slug path implies a parent... slug_parts = split_slug(document_slug) - if not slug_parts['parent']: + if not slug_parts["parent"]: # Apparently, this is a root page! parent_doc = None else: # There's a parent implied, so make sure we can find it. - parent_doc = get_object_or_404(Document, locale=document_locale, - slug=slug_parts['parent']) + parent_doc = get_object_or_404( + Document, locale=document_locale, slug=slug_parts["parent"] + ) # Create and save the new document; we'll revise it immediately. - doc = Document(slug=document_slug, locale=document_locale, - title=data.get('title', document_slug), - parent_topic=parent_doc) + doc = Document( + slug=document_slug, + locale=document_locale, + title=data.get("title", document_slug), + parent_topic=parent_doc, + ) doc.save() section_id = None # No section editing for new document! is_new = True new_rev = doc.revise(request.user, data, section_id) - doc.schedule_rendering('max-age=0') + doc.schedule_rendering("max-age=0") - request.authkey.log('created' if is_new else 'updated', - new_rev, data.get('summary', None)) + request.authkey.log( + "created" if is_new else "updated", new_rev, data.get("summary", None) + ) resp = HttpResponse() if is_new: - resp['Location'] = request.build_absolute_uri(doc.get_absolute_url()) + resp["Location"] = request.build_absolute_uri(doc.get_absolute_url()) resp.status_code = 201 else: resp.status_code = 205 diff --git a/kuma/wiki/views/edit.py b/kuma/wiki/views/edit.py index d9d0a799659..c85e7575d28 100644 --- a/kuma/wiki/views/edit.py +++ b/kuma/wiki/views/edit.py @@ -15,30 +15,43 @@ import kuma.wiki.content from kuma.attachments.forms import AttachmentRevisionForm -from kuma.core.decorators import (block_banned_ips, block_user_agents, - ensure_wiki_domain, login_required) +from kuma.core.decorators import ( + block_banned_ips, + block_user_agents, + ensure_wiki_domain, + login_required, +) from kuma.core.urlresolvers import reverse from kuma.core.utils import urlparams from .translate import translate from .utils import document_form_initial, split_slug -from ..decorators import (check_readonly, prevent_indexing, - process_document_path) +from ..decorators import check_readonly, prevent_indexing, process_document_path from ..forms import DocumentForm, RevisionForm from ..models import Document, Revision @xframe_options_sameorigin -def _edit_document_collision(request, orig_rev, curr_rev, is_async_submit, - is_raw, rev_form, doc_form, section_id, rev, doc): +def _edit_document_collision( + request, + orig_rev, + curr_rev, + is_async_submit, + is_raw, + rev_form, + doc_form, + section_id, + rev, + doc, +): """ Handle when a mid-air collision is detected upon submission """ # Process the content as if it were about to be saved, so that the # html_diff is close as possible. - content = (kuma.wiki.content.parse(request.POST['content']) - .injectSectionIDs() - .serialize()) + content = ( + kuma.wiki.content.parse(request.POST["content"]).injectSectionIDs().serialize() + ) # Process the original content for a diff, extracting a section if we're # editing one. @@ -52,34 +65,34 @@ def _edit_document_collision(request, orig_rev, curr_rev, is_async_submit, # When dealing with the raw content API, we need to signal the conflict # differently so the client-side can escape out to a conflict # resolution UI. - response = HttpResponse('CONFLICT') + response = HttpResponse("CONFLICT") response.status_code = 409 return response # Make this response iframe-friendly so we can hack around the # save-and-edit iframe button context = { - 'collision': True, - 'revision_form': rev_form, - 'document_form': doc_form, - 'content': content, - 'current_content': curr_content, - 'section_id': section_id, - 'original_revision': orig_rev, - 'current_revision': curr_rev, - 'revision': rev, - 'document': doc, + "collision": True, + "revision_form": rev_form, + "document_form": doc_form, + "content": content, + "current_content": curr_content, + "section_id": section_id, + "original_revision": orig_rev, + "current_revision": curr_rev, + "revision": rev, + "document": doc, } - return render(request, 'wiki/edit.html', context) + return render(request, "wiki/edit.html", context) @ensure_wiki_domain @newrelic.agent.function_trace() @never_cache @block_user_agents -@require_http_methods(['GET', 'POST']) +@require_http_methods(["GET", "POST"]) @login_required # TODO: Stop repeating this knowledge here and in Document.allows_editing_by. -@ratelimit(key='user', rate='60/m', block=True) +@ratelimit(key="user", rate="60/m", block=True) @block_banned_ips @csp_update(SCRIPT_SRC="'unsafe-eval'") # Required until CKEditor 4.7 @process_document_path @@ -89,55 +102,54 @@ def edit(request, document_slug, document_locale): """ Create a new revision of a wiki document, or edit document metadata. """ - doc = get_object_or_404(Document, - locale=document_locale, - slug=document_slug) + doc = get_object_or_404(Document, locale=document_locale, slug=document_slug) # If this document has a parent, then the edit is handled by the # translate view. Pass it on. if doc.parent and doc.parent.id != doc.id: - return translate(request, doc.parent.slug, doc.locale, - bypass_process_document_path=True) + return translate( + request, doc.parent.slug, doc.locale, bypass_process_document_path=True + ) - rev = doc.current_revision or doc.revisions.order_by('-created', '-id')[0] + rev = doc.current_revision or doc.revisions.order_by("-created", "-id")[0] # Keep hold of the full post slug slug_dict = split_slug(document_slug) # Update the slug, removing the parent path, and # *only* using the last piece. # This is only for the edit form. - rev.slug = slug_dict['specific'] + rev.slug = slug_dict["specific"] - section_id = request.GET.get('section', None) + section_id = request.GET.get("section", None) if section_id and not request.is_ajax(): return HttpResponse(ugettext("Sections may only be edited inline.")) - disclose_description = bool(request.GET.get('opendescription')) + disclose_description = bool(request.GET.get("opendescription")) doc_form = rev_form = None - rev_form = RevisionForm(request=request, - instance=rev, - initial={'based_on': rev.id, - 'current_rev': rev.id, - 'comment': ''}, - section_id=section_id) + rev_form = RevisionForm( + request=request, + instance=rev, + initial={"based_on": rev.id, "current_rev": rev.id, "comment": ""}, + section_id=section_id, + ) if doc.allows_editing_by(request.user): doc_form = DocumentForm(initial=document_form_initial(doc)) # Need to make check *here* to see if this could have a translation parent show_translation_parent_block = ( - (document_locale != settings.WIKI_DEFAULT_LANGUAGE) and - (not doc.parent_id)) + document_locale != settings.WIKI_DEFAULT_LANGUAGE + ) and (not doc.parent_id) - if request.method == 'GET': + if request.method == "GET": if not (rev_form or doc_form): # You can't do anything on this page, so get lost. raise PermissionDenied else: # POST is_async_submit = request.is_ajax() - is_raw = request.GET.get('raw', False) - need_edit_links = request.GET.get('edit_links', False) - parent_id = request.POST.get('parent_id', '') + is_raw = request.GET.get("raw", False) + need_edit_links = request.GET.get("edit_links", False) + parent_id = request.POST.get("parent_id", "") # Attempt to set a parent if show_translation_parent_block and parent_id: @@ -149,41 +161,43 @@ def edit(request, document_slug, document_locale): # Comparing against localized names for the Save button bothers me, so # I embedded a hidden input: - which_form = request.POST.get('form-type') + which_form = request.POST.get("form-type") - if which_form == 'doc': + if which_form == "doc": if doc.allows_editing_by(request.user): post_data = request.POST.copy() - post_data.update({'locale': document_locale}) + post_data.update({"locale": document_locale}) doc_form = DocumentForm(post_data, instance=doc) if doc_form.is_valid(): # if must be here for section edits - if 'slug' in post_data: - post_data['slug'] = '/'.join([slug_dict['parent'], - post_data['slug']]) + if "slug" in post_data: + post_data["slug"] = "/".join( + [slug_dict["parent"], post_data["slug"]] + ) # Get the possibly new slug for the imminent redirection: doc = doc_form.save(parent=None) - return redirect(urlparams(doc.get_edit_url(), - opendescription=1)) + return redirect(urlparams(doc.get_edit_url(), opendescription=1)) disclose_description = True else: raise PermissionDenied - elif which_form == 'rev': + elif which_form == "rev": post_data = request.POST.copy() - rev_form = RevisionForm(request=request, - data=post_data, - is_async_submit=is_async_submit, - section_id=section_id) + rev_form = RevisionForm( + request=request, + data=post_data, + is_async_submit=is_async_submit, + section_id=section_id, + ) rev_form.instance.document = doc # for rev_form.clean() # Come up with the original revision to which these changes # would be applied. - orig_rev_id = request.POST.get('current_rev', False) + orig_rev_id = request.POST.get("current_rev", False) if orig_rev_id is False: orig_rev = None else: @@ -195,11 +209,12 @@ def edit(request, document_slug, document_locale): # If this was an Ajax POST, then return a JsonResponse if is_async_submit: # Was there a mid-air collision? - if 'current_rev' in rev_form._errors: + if "current_rev" in rev_form._errors: # Make the error message safe so the '<' and '>' don't # get turned into '<' and '>', respectively - rev_form.errors['current_rev'][0] = mark_safe( - rev_form.errors['current_rev'][0]) + rev_form.errors["current_rev"][0] = mark_safe( + rev_form.errors["current_rev"][0] + ) errors = [rev_form.errors[key][0] for key in rev_form.errors.keys()] data = { @@ -210,70 +225,74 @@ def edit(request, document_slug, document_locale): return JsonResponse(data=data) # Jump out to a function to escape indentation hell return _edit_document_collision( - request, orig_rev, curr_rev, is_async_submit, - is_raw, rev_form, doc_form, section_id, - rev, doc) + request, + orig_rev, + curr_rev, + is_async_submit, + is_raw, + rev_form, + doc_form, + section_id, + rev, + doc, + ) # Was this an Ajax submission that was marked as spam? - if is_async_submit and '__all__' in rev_form._errors: + if is_async_submit and "__all__" in rev_form._errors: # Return a JsonResponse data = { "error": True, - "error_message": mark_safe(rev_form.errors['__all__'][0]), + "error_message": mark_safe(rev_form.errors["__all__"][0]), "new_revision_id": curr_rev.id, } return JsonResponse(data=data) if rev_form.is_valid(): rev_form.save(doc) - if (is_raw and orig_rev is not None and - curr_rev.id != orig_rev.id): + if is_raw and orig_rev is not None and curr_rev.id != orig_rev.id: # If this is the raw view, and there was an original # revision, but the original revision differed from the # current revision at start of editing, we should tell # the client to refresh the page. - response = HttpResponse('RESET') - response['X-Frame-Options'] = 'SAMEORIGIN' + response = HttpResponse("RESET") + response["X-Frame-Options"] = "SAMEORIGIN" response.status_code = 205 return response # Is this an Ajax POST? if is_async_submit: # This is the most recent revision id - new_rev_id = rev.document.revisions.order_by('-id').first().id - data = { - "error": False, - "new_revision_id": new_rev_id - } + new_rev_id = rev.document.revisions.order_by("-id").first().id + data = {"error": False, "new_revision_id": new_rev_id} return JsonResponse(data) if rev_form.instance.is_approved: - view = 'wiki.document' + view = "wiki.document" else: - view = 'wiki.document_revisions' + view = "wiki.document_revisions" # Construct the redirect URL, adding any needed parameters url = reverse(view, args=[doc.slug], locale=doc.locale) params = {} if is_raw: - params['raw'] = 'true' + params["raw"] = "true" if need_edit_links: # Only need to carry over ?edit_links with ?raw, # because they're on by default in the normal UI - params['edit_links'] = 'true' + params["edit_links"] = "true" if section_id: # If a section was edited, and we're using the raw # content API, constrain to that section. - params['section'] = section_id + params["section"] = section_id # Parameter for the document saved, so that we can delete the cached draft on load - params['rev_saved'] = curr_rev.id if curr_rev else '' - url = '%s?%s' % (url, urlencode(params)) + params["rev_saved"] = curr_rev.id if curr_rev else "" + url = "%s?%s" % (url, urlencode(params)) if not is_raw and section_id: # If a section was edited, jump to the section anchor # if we're not getting raw content. - url = '%s#%s' % (url, section_id) + url = "%s#%s" % (url, section_id) return redirect(url) - parent_path = parent_slug = '' - if slug_dict['parent']: - parent_slug = slug_dict['parent'] + parent_path = parent_slug = "" + if slug_dict["parent"]: + parent_slug = slug_dict["parent"] if doc.parent_topic_id: parent_doc = Document.objects.get(pk=doc.parent_topic_id) @@ -281,14 +300,14 @@ def edit(request, document_slug, document_locale): parent_slug = parent_doc.slug context = { - 'revision_form': rev_form, - 'document_form': doc_form, - 'section_id': section_id, - 'disclose_description': disclose_description, - 'parent_slug': parent_slug, - 'parent_path': parent_path, - 'revision': rev, - 'document': doc, - 'attachment_form': AttachmentRevisionForm(), + "revision_form": rev_form, + "document_form": doc_form, + "section_id": section_id, + "disclose_description": disclose_description, + "parent_slug": parent_slug, + "parent_path": parent_path, + "revision": rev, + "document": doc, + "attachment_form": AttachmentRevisionForm(), } - return render(request, 'wiki/edit.html', context) + return render(request, "wiki/edit.html", context) diff --git a/kuma/wiki/views/legacy.py b/kuma/wiki/views/legacy.py index b33aadd3073..a973f854e02 100644 --- a/kuma/wiki/views/legacy.py +++ b/kuma/wiki/views/legacy.py @@ -1,5 +1,3 @@ - - from django.conf import settings from django.http import Http404 from django.shortcuts import redirect @@ -12,6 +10,7 @@ # Legacy MindTouch redirects. + def mindtouch_namespace_to_kuma_url(locale, namespace, slug): """ Convert MindTouch namespace URLs to Kuma URLs. @@ -21,32 +20,31 @@ def mindtouch_namespace_to_kuma_url(locale, namespace, slug): If the locale cannot be correctly determined, fall back to en-US """ new_locale = new_slug = None - if namespace in ('Talk', 'Project', 'Project_talk'): + if namespace in ("Talk", "Project", "Project_talk"): # These namespaces carry the old locale in their URL, which # simplifies figuring out where to send them. - mt_locale, _, doc_slug = slug.partition('/') - new_locale = settings.MT_TO_KUMA_LOCALE_MAP.get(mt_locale, 'en-US') - new_slug = '%s:%s' % (namespace, doc_slug) - elif namespace == 'User': + mt_locale, _, doc_slug = slug.partition("/") + new_locale = settings.MT_TO_KUMA_LOCALE_MAP.get(mt_locale, "en-US") + new_slug = "%s:%s" % (namespace, doc_slug) + elif namespace == "User": # For users, we look up the latest revision and get the locale # from there. - new_slug = '%s:%s' % (namespace, slug) + new_slug = "%s:%s" % (namespace, slug) try: # TODO: Tests do not include a matching revision - rev = (Revision.objects.filter(document__slug=new_slug) - .latest('created')) + rev = Revision.objects.filter(document__slug=new_slug).latest("created") new_locale = rev.document.locale except Revision.DoesNotExist: # If that doesn't work, bail out to en-US. - new_locale = 'en-US' + new_locale = "en-US" else: # Templates, etc. don't actually have a locale, so we give # them the default. - new_locale = 'en-US' - new_slug = '%s:%s' % (namespace, slug) + new_locale = "en-US" + new_slug = "%s:%s" % (namespace, slug) if new_locale: # TODO: new_locale is unused, no alternate branch - new_url = '/%s/docs/%s' % (locale, new_slug) + new_url = "/%s/docs/%s" % (locale, new_slug) return new_url @@ -57,22 +55,22 @@ def mindtouch_to_kuma_url(locale, path): If there is an appropriate Kuma URL, then it is returned. If there is no appropriate Kuma URL, then None is returned. """ - if path.startswith('%s/' % locale): + if path.startswith("%s/" % locale): # Convert from Django-based LocaleMiddleware path to zamboni/amo style - path = path.replace('%s/' % locale, '', 1) + path = path.replace("%s/" % locale, "", 1) - if path.startswith('Template:MindTouch'): + if path.startswith("Template:MindTouch"): # MindTouch's default templates. There shouldn't be links to # them anywhere in the wild, but just in case we 404 them. # TODO: Tests don't exercise this branch return None - if path.endswith('/'): + if path.endswith("/"): # If there's a trailing slash, snip it off. path = path[:-1] - if ':' in path: - namespace, _, slug = path.partition(':') + if ":" in path: + namespace, _, slug = path.partition(":") # The namespaces (Talk:, User:, etc.) get their own # special-case handling. # TODO: Test invalid namespace @@ -104,8 +102,8 @@ def mindtouch_to_kuma_redirect(request, path): locale = request.LANGUAGE_CODE url = mindtouch_to_kuma_url(locale, path) if url: - if 'view' in request.GET: - url = '%s$%s' % (url, request.GET['view']) + if "view" in request.GET: + url = "%s$%s" % (url, request.GET["view"]) return redirect(url, permanent=True) else: raise Http404 diff --git a/kuma/wiki/views/list.py b/kuma/wiki/views/list.py index 0790565a877..c82371b8cee 100644 --- a/kuma/wiki/views/list.py +++ b/kuma/wiki/views/list.py @@ -1,24 +1,24 @@ - - from django.shortcuts import get_list_or_404, get_object_or_404, render from django.views.decorators.http import require_GET from ratelimit.decorators import ratelimit -from kuma.core.decorators import (block_user_agents, ensure_wiki_domain, - shared_cache_control) +from kuma.core.decorators import ( + block_user_agents, + ensure_wiki_domain, + shared_cache_control, +) from kuma.core.utils import paginate from ..constants import DOCUMENTS_PER_PAGE from ..decorators import prevent_indexing, process_document_path -from ..models import (Document, DocumentTag, LocalizationTag, ReviewTag, - Revision) +from ..models import Document, DocumentTag, LocalizationTag, ReviewTag, Revision @ensure_wiki_domain @shared_cache_control @block_user_agents @require_GET -@ratelimit(key='user_or_ip', rate='40/m', block=True) +@ratelimit(key="user_or_ip", rate="40/m", block=True) def documents(request, tag=None): """ List wiki documents depending on the optionally given tag. @@ -32,125 +32,121 @@ def documents(request, tag=None): if matching_tag.name.lower() == tag.lower(): tag_obj = matching_tag break - docs = Document.objects.filter_for_list(locale=request.LANGUAGE_CODE, - tag=tag_obj) + docs = Document.objects.filter_for_list(locale=request.LANGUAGE_CODE, tag=tag_obj) paginated_docs = paginate(request, docs, per_page=DOCUMENTS_PER_PAGE) context = { - 'documents': paginated_docs, - 'tag': tag, + "documents": paginated_docs, + "tag": tag, } - return render(request, 'wiki/list/documents.html', context) + return render(request, "wiki/list/documents.html", context) @ensure_wiki_domain @shared_cache_control @block_user_agents @require_GET -@ratelimit(key='user_or_ip', rate='40/m', block=True) +@ratelimit(key="user_or_ip", rate="40/m", block=True) def tags(request): """ Returns listing of all tags """ - tags = DocumentTag.objects.order_by('name') + tags = DocumentTag.objects.order_by("name") tags = paginate(request, tags, per_page=DOCUMENTS_PER_PAGE) - return render(request, 'wiki/list/tags.html', {'tags': tags}) + return render(request, "wiki/list/tags.html", {"tags": tags}) @ensure_wiki_domain @shared_cache_control @block_user_agents @require_GET -@ratelimit(key='user_or_ip', rate='40/m', block=True) +@ratelimit(key="user_or_ip", rate="40/m", block=True) def needs_review(request, tag=None): """ Lists wiki documents with revisions flagged for review """ tag_obj = tag and get_object_or_404(ReviewTag, name=tag) or None - docs = Document.objects.filter_for_review(locale=request.LANGUAGE_CODE, - tag=tag_obj) + docs = Document.objects.filter_for_review(locale=request.LANGUAGE_CODE, tag=tag_obj) paginated_docs = paginate(request, docs, per_page=DOCUMENTS_PER_PAGE) context = { - 'documents': paginated_docs, - 'count': docs.count(), - 'tag': tag_obj, - 'tag_name': tag, + "documents": paginated_docs, + "count": docs.count(), + "tag": tag_obj, + "tag_name": tag, } - return render(request, 'wiki/list/needs_review.html', context) + return render(request, "wiki/list/needs_review.html", context) @ensure_wiki_domain @shared_cache_control @block_user_agents @require_GET -@ratelimit(key='user_or_ip', rate='40/m', block=True) +@ratelimit(key="user_or_ip", rate="40/m", block=True) def with_localization_tag(request, tag=None): """ Lists wiki documents with localization tag """ tag_obj = tag and get_object_or_404(LocalizationTag, name=tag) or None docs = Document.objects.filter_with_localization_tag( - locale=request.LANGUAGE_CODE, tag=tag_obj) + locale=request.LANGUAGE_CODE, tag=tag_obj + ) paginated_docs = paginate(request, docs, per_page=DOCUMENTS_PER_PAGE) context = { - 'documents': paginated_docs, - 'count': docs.count(), - 'tag': tag_obj, - 'tag_name': tag, + "documents": paginated_docs, + "count": docs.count(), + "tag": tag_obj, + "tag_name": tag, } - return render(request, 'wiki/list/with_localization_tags.html', context) + return render(request, "wiki/list/with_localization_tags.html", context) @ensure_wiki_domain @shared_cache_control @block_user_agents @require_GET -@ratelimit(key='user_or_ip', rate='40/m', block=True) +@ratelimit(key="user_or_ip", rate="40/m", block=True) def with_errors(request): """ Lists wiki documents with (KumaScript) errors """ - docs = Document.objects.filter_for_list(locale=request.LANGUAGE_CODE, - errors=True) + docs = Document.objects.filter_for_list(locale=request.LANGUAGE_CODE, errors=True) paginated_docs = paginate(request, docs, per_page=DOCUMENTS_PER_PAGE) context = { - 'documents': paginated_docs, - 'errors': True, + "documents": paginated_docs, + "errors": True, } - return render(request, 'wiki/list/documents.html', context) + return render(request, "wiki/list/documents.html", context) @ensure_wiki_domain @shared_cache_control @block_user_agents @require_GET -@ratelimit(key='user_or_ip', rate='40/m', block=True) +@ratelimit(key="user_or_ip", rate="40/m", block=True) def without_parent(request): """Lists wiki documents without parent (no English source document)""" - docs = Document.objects.filter_for_list(locale=request.LANGUAGE_CODE, - noparent=True) + docs = Document.objects.filter_for_list(locale=request.LANGUAGE_CODE, noparent=True) paginated_docs = paginate(request, docs, per_page=DOCUMENTS_PER_PAGE) context = { - 'documents': paginated_docs, - 'noparent': True, + "documents": paginated_docs, + "noparent": True, } - return render(request, 'wiki/list/documents.html', context) + return render(request, "wiki/list/documents.html", context) @ensure_wiki_domain @shared_cache_control @block_user_agents @require_GET -@ratelimit(key='user_or_ip', rate='400/m', block=True) +@ratelimit(key="user_or_ip", rate="400/m", block=True) def top_level(request): """Lists documents directly under /docs/""" - docs = Document.objects.filter_for_list(locale=request.LANGUAGE_CODE, - toplevel=True) + docs = Document.objects.filter_for_list(locale=request.LANGUAGE_CODE, toplevel=True) paginated_docs = paginate(request, docs, per_page=DOCUMENTS_PER_PAGE) context = { - 'documents': paginated_docs, - 'toplevel': True, + "documents": paginated_docs, + "toplevel": True, } - return render(request, 'wiki/list/documents.html', context) + return render(request, "wiki/list/documents.html", context) @ensure_wiki_domain @@ -159,39 +155,47 @@ def top_level(request): @require_GET @process_document_path @prevent_indexing -@ratelimit(key='user_or_ip', rate='20/m', block=True) +@ratelimit(key="user_or_ip", rate="20/m", block=True) def revisions(request, document_slug, document_locale): """ List all the revisions of a given document. """ - locale = request.GET.get('locale', document_locale) + locale = request.GET.get("locale", document_locale) # Load document with only fields for history display - doc_query = (Document.objects - .only('id', 'locale', 'slug', 'title', - 'current_revision_id', - 'parent__slug', 'parent__locale') - .select_related('parent') - .exclude(current_revision__isnull=True) - .filter(locale=locale, slug=document_slug)) + doc_query = ( + Document.objects.only( + "id", + "locale", + "slug", + "title", + "current_revision_id", + "parent__slug", + "parent__locale", + ) + .select_related("parent") + .exclude(current_revision__isnull=True) + .filter(locale=locale, slug=document_slug) + ) document = get_object_or_404(doc_query) # Process the requested page size - per_page = request.GET.get('limit', 10) - if not request.user.is_authenticated and per_page == 'all': - return render(request, '403.html', - {'reason': 'revisions_login_required'}, status=403) + per_page = request.GET.get("limit", 10) + if not request.user.is_authenticated and per_page == "all": + return render( + request, "403.html", {"reason": "revisions_login_required"}, status=403 + ) # Get ordered revision IDs - revision_ids = list(document.revisions - .order_by('-created', '-id') - .values_list('id', flat=True)) + revision_ids = list( + document.revisions.order_by("-created", "-id").values_list("id", flat=True) + ) # Create pairs (this revision, previous revision) revision_pairs = list(zip(revision_ids, revision_ids[1:] + [None])) # Paginate the revision pairs, or use all of them - if per_page == 'all': + if per_page == "all": page = None selected_revision_pairs = revision_pairs else: @@ -206,7 +210,7 @@ def revisions(request, document_slug, document_locale): # Include original English revision of the first translation earliest_id, earliest_prev_id = selected_revision_pairs[-1] if earliest_prev_id is None and document.parent: - earliest = Revision.objects.only('based_on').get(id=earliest_id) + earliest = Revision.objects.only("based_on").get(id=earliest_id) if earliest.based_on is not None: selected_revision_pairs[-1] = (earliest_id, earliest.based_on_id) selected_revision_pairs.append((earliest.based_on_id, None)) @@ -216,18 +220,26 @@ def revisions(request, document_slug, document_locale): previous_id = selected_revision_pairs[-1][1] if previous_id is not None: selected_revision_ids.append(previous_id) - selected_revisions = (Revision.objects - .only('id', 'slug', 'created', 'comment', - 'document__slug', 'document__locale', - 'creator__username', 'creator__is_active') - .select_related('document', 'creator') - .filter(id__in=selected_revision_ids)) + selected_revisions = ( + Revision.objects.only( + "id", + "slug", + "created", + "comment", + "document__slug", + "document__locale", + "creator__username", + "creator__is_active", + ) + .select_related("document", "creator") + .filter(id__in=selected_revision_ids) + ) revisions = {rev.id: rev for rev in selected_revisions} context = { - 'selected_revision_pairs': selected_revision_pairs, - 'revisions': revisions, - 'document': document, - 'page': page, + "selected_revision_pairs": selected_revision_pairs, + "revisions": revisions, + "document": document, + "page": page, } - return render(request, 'wiki/list/revisions.html', context) + return render(request, "wiki/list/revisions.html", context) diff --git a/kuma/wiki/views/misc.py b/kuma/wiki/views/misc.py index 5ef974e36cb..84b40c6cfd5 100644 --- a/kuma/wiki/views/misc.py +++ b/kuma/wiki/views/misc.py @@ -1,13 +1,14 @@ - - import newrelic.agent from django.http import HttpResponseBadRequest, JsonResponse from django.shortcuts import render from django.utils.translation import ugettext_lazy as _ from django.views.decorators.http import require_GET -from kuma.core.decorators import (block_user_agents, ensure_wiki_domain, - shared_cache_control) +from kuma.core.decorators import ( + block_user_agents, + ensure_wiki_domain, + shared_cache_control, +) from ..constants import ALLOWED_TAGS, REDIRECT_CONTENT from ..decorators import allow_CORS_GET @@ -21,19 +22,23 @@ def ckeditor_config(request): """ Return ckeditor config from database """ - default_config = EditorToolbar.objects.filter(name='default') + default_config = EditorToolbar.objects.filter(name="default") if default_config.exists(): code = default_config[0].code else: - code = '' + code = "" context = { - 'editor_config': code, - 'redirect_pattern': REDIRECT_CONTENT, - 'allowed_tags': ' '.join(ALLOWED_TAGS), + "editor_config": code, + "redirect_pattern": REDIRECT_CONTENT, + "allowed_tags": " ".join(ALLOWED_TAGS), } - return render(request, 'wiki/ckeditor_config.js', context, - content_type='application/x-javascript') + return render( + request, + "wiki/ckeditor_config.js", + context, + content_type="application/x-javascript", + ) @shared_cache_control @@ -45,24 +50,29 @@ def autosuggest_documents(request): """ Returns the closest title matches for front-end autosuggests """ - partial_title = request.GET.get('term', '') - locale = request.GET.get('locale', False) - current_locale = request.GET.get('current_locale', False) - exclude_current_locale = request.GET.get('exclude_current_locale', False) + partial_title = request.GET.get("term", "") + locale = request.GET.get("locale", False) + current_locale = request.GET.get("current_locale", False) + exclude_current_locale = request.GET.get("exclude_current_locale", False) if not partial_title: # Only handle actual autosuggest requests, not requests for a # memory-busting list of all documents. - return HttpResponseBadRequest(_('Autosuggest requires a partial ' - 'title. For a full document ' - 'index, see the main page.')) + return HttpResponseBadRequest( + _( + "Autosuggest requires a partial " + "title. For a full document " + "index, see the main page." + ) + ) # Retrieve all documents that aren't redirects - docs = (Document.objects.extra(select={'length': 'Length(slug)'}) - .filter(title__icontains=partial_title, - is_redirect=0) - .exclude(slug__icontains='Talk:') # Remove old talk pages - .order_by('title', 'length')) + docs = ( + Document.objects.extra(select={"length": "Length(slug)"}) + .filter(title__icontains=partial_title, is_redirect=0) + .exclude(slug__icontains="Talk:") # Remove old talk pages + .order_by("title", "length") + ) # All locales are assumed, unless a specific locale is requested or banned if locale: @@ -76,7 +86,7 @@ def autosuggest_documents(request): docs_list = [] for doc in docs: data = doc.get_json_data() - data['label'] += ' [' + doc.locale + ']' + data["label"] += " [" + doc.locale + "]" docs_list.append(data) return JsonResponse(docs_list, safe=False) diff --git a/kuma/wiki/views/revision.py b/kuma/wiki/views/revision.py index 997878020fc..761642cbd14 100644 --- a/kuma/wiki/views/revision.py +++ b/kuma/wiki/views/revision.py @@ -3,8 +3,12 @@ import newrelic.agent from django.conf import settings from django.core.exceptions import PermissionDenied -from django.http import (Http404, HttpResponse, HttpResponseBadRequest, - HttpResponseForbidden) +from django.http import ( + Http404, + HttpResponse, + HttpResponseBadRequest, + HttpResponseForbidden, +) from django.shortcuts import get_object_or_404, redirect, render from django.utils.translation import ugettext_lazy as _ from django.views.decorators.cache import never_cache @@ -15,8 +19,12 @@ from rest_framework.authentication import TokenAuthentication from rest_framework.decorators import api_view, authentication_classes -from kuma.core.decorators import (block_user_agents, ensure_wiki_domain, - login_required, shared_cache_control) +from kuma.core.decorators import ( + block_user_agents, + ensure_wiki_domain, + login_required, + shared_cache_control, +) from kuma.core.utils import smart_int from .. import kumascript @@ -32,20 +40,22 @@ @block_user_agents @prevent_indexing @process_document_path -@ratelimit(key='user_or_ip', rate='15/m', block=True) +@ratelimit(key="user_or_ip", rate="15/m", block=True) def revision(request, document_slug, document_locale, revision_id): """ View a wiki document revision. """ - rev = get_object_or_404(Revision.objects.select_related('document'), - pk=revision_id, - document__slug=document_slug) + rev = get_object_or_404( + Revision.objects.select_related("document"), + pk=revision_id, + document__slug=document_slug, + ) context = { - 'document': rev.document, - 'revision': rev, - 'comment': format_comment(rev), + "document": rev.document, + "revision": rev, + "comment": format_comment(rev), } - return render(request, 'wiki/revision.html', context) + return render(request, "wiki/revision.html", context) @ensure_wiki_domain @@ -60,31 +70,33 @@ def preview(request): doc = None render_preview = True - wiki_content = request.POST.get('content', '') - doc_id = request.POST.get('doc_id') + wiki_content = request.POST.get("content", "") + doc_id = request.POST.get("doc_id") if doc_id: doc = Document.objects.get(id=doc_id) if doc and doc.defer_rendering: render_preview = False else: - render_preview = kumascript.should_use_rendered(doc, - request.GET, - html=wiki_content) + render_preview = kumascript.should_use_rendered( + doc, request.GET, html=wiki_content + ) if render_preview: - wiki_content, kumascript_errors = kumascript.post(request, - wiki_content, - request.LANGUAGE_CODE) + wiki_content, kumascript_errors = kumascript.post( + request, wiki_content, request.LANGUAGE_CODE + ) # TODO: Get doc ID from JSON. context = { - 'content': wiki_content, - 'title': request.POST.get('title', ''), - 'kumascript_errors': kumascript_errors, - 'macro_sources': (kumascript.macro_sources(force_lowercase_keys=True) - if kumascript_errors else - None), + "content": wiki_content, + "title": request.POST.get("title", ""), + "kumascript_errors": kumascript_errors, + "macro_sources": ( + kumascript.macro_sources(force_lowercase_keys=True) + if kumascript_errors + else None + ), } - return render(request, 'wiki/preview.html', context) + return render(request, "wiki/preview.html", context) @ensure_wiki_domain @@ -94,25 +106,23 @@ def preview(request): @xframe_options_sameorigin @process_document_path @prevent_indexing -@ratelimit(key='user_or_ip', rate='15/m', block=True) +@ratelimit(key="user_or_ip", rate="15/m", block=True) def compare(request, document_slug, document_locale): """ Compare two wiki document revisions. The ids are passed as query string parameters (to and from). """ - locale = request.GET.get('locale', document_locale) - if 'from' not in request.GET or 'to' not in request.GET: + locale = request.GET.get("locale", document_locale) + if "from" not in request.GET or "to" not in request.GET: raise Http404 - doc = get_object_or_404(Document, - locale=locale, - slug=document_slug) + doc = get_object_or_404(Document, locale=locale, slug=document_slug) - from_id = smart_int(request.GET.get('from')) - to_id = smart_int(request.GET.get('to')) + from_id = smart_int(request.GET.get("from")) + to_id = smart_int(request.GET.get("to")) - revisions = Revision.objects.prefetch_related('document') + revisions = Revision.objects.prefetch_related("document") # It should also be possible to compare from the parent document revision try: revision_from = revisions.get(id=from_id, document=doc) @@ -122,15 +132,15 @@ def compare(request, document_slug, document_locale): revision_to = get_object_or_404(revisions, id=to_id, document=doc) context = { - 'document': doc, - 'revision_from': revision_from, - 'revision_to': revision_to, + "document": doc, + "revision_from": revision_from, + "revision_to": revision_to, } - if request.GET.get('raw', False): - template = 'wiki/includes/revision_diff_table.html' + if request.GET.get("raw", False): + template = "wiki/includes/revision_diff_table.html" else: - template = 'wiki/compare_revisions.html' + template = "wiki/compare_revisions.html" return render(request, template, context) @@ -146,11 +156,9 @@ def quick_review(request, document_slug, document_locale): Quickly mark a revision as no longer needing a particular type of review. """ - doc = get_object_or_404(Document, - locale=document_locale, - slug=document_slug) + doc = get_object_or_404(Document, locale=document_locale, slug=document_slug) - rev_id = request.POST.get('revision_id') + rev_id = request.POST.get("revision_id") if not rev_id: raise Http404 @@ -166,27 +174,27 @@ def quick_review(request, document_slug, document_locale): needs_technical = rev.needs_technical_review needs_editorial = rev.needs_editorial_review - request_technical = request.POST.get('request_technical', False) - request_editorial = request.POST.get('request_editorial', False) + request_technical = request.POST.get("request_technical", False) + request_editorial = request.POST.get("request_editorial", False) messages = [] new_tags = [] if needs_technical: - new_tags.append('technical') + new_tags.append("technical") if needs_editorial: - new_tags.append('editorial') + new_tags.append("editorial") if needs_technical and not request_technical: - new_tags.remove('technical') - messages.append('Technical review completed.') + new_tags.remove("technical") + messages.append("Technical review completed.") if needs_editorial and not request_editorial: - new_tags.remove('editorial') - messages.append('Editorial review completed.') + new_tags.remove("editorial") + messages.append("Editorial review completed.") if messages: # We approved something, make the new revision. - data = {'summary': ' '.join(messages), 'comment': ' '.join(messages)} + data = {"summary": " ".join(messages), "comment": " ".join(messages)} new_rev = doc.revise(request.user, data=data) if new_tags: new_rev.review_tags.set(*new_tags) @@ -197,7 +205,7 @@ def quick_review(request, document_slug, document_locale): @never_cache @ensure_wiki_domain -@api_view(['GET', 'HEAD', 'POST']) +@api_view(["GET", "HEAD", "POST"]) @authentication_classes([TokenAuthentication]) @process_document_path def revision_api(request, document_slug, document_locale): @@ -209,21 +217,18 @@ def revision_api(request, document_slug, document_locale): POST requests, but conditional request handling is only intended for POST requests to avoid collisions. """ - doc = get_object_or_404( - Document, - slug=document_slug, - locale=document_locale - ) + doc = get_object_or_404(Document, slug=document_slug, locale=document_locale) - if request.method == 'POST': + if request.method == "POST": if not (request.auth and request.user.is_authenticated): return HttpResponseForbidden() content_type = request.content_type - if content_type.startswith('application/json'): + if content_type.startswith("application/json"): encoding = request.encoding or settings.DEFAULT_CHARSET data = json.loads(request.body.decode(encoding=encoding)) - elif (content_type.startswith('multipart/form-data') or - content_type.startswith('application/x-www-form-urlencoded')): + elif content_type.startswith("multipart/form-data") or content_type.startswith( + "application/x-www-form-urlencoded" + ): data = request.POST else: return HttpResponseBadRequest( @@ -232,24 +237,25 @@ def revision_api(request, document_slug, document_locale): ) return do_revision_api_post(request, doc, data) - mode = request.GET.get('mode') - select_macros = request.GET.get('macros') + mode = request.GET.get("mode") + select_macros = request.GET.get("macros") if mode: - if mode not in ('render', 'remove'): + if mode not in ("render", "remove"): return HttpResponseBadRequest( - 'The "mode" query parameter must be "render" or "remove".') + 'The "mode" query parameter must be "render" or "remove".' + ) if not select_macros: return HttpResponseBadRequest( - 'Please specify one or more comma-separated macro names via ' - 'the "macros" query parameter.') + "Please specify one or more comma-separated macro names via " + 'the "macros" query parameter.' + ) elif select_macros: - return HttpResponseBadRequest( - 'Please specify a "mode" query parameter.') + return HttpResponseBadRequest('Please specify a "mode" query parameter.') if select_macros: # Convert potentially comma-separated macro names into a list. - select_macros = select_macros.replace(',', ' ').split() + select_macros = select_macros.replace(",", " ").split() return do_revision_api_get(request, doc, mode, select_macros) @@ -269,12 +275,13 @@ def do_revision_api_get(request, doc, mode, select_macros): """ if mode and select_macros: html, _ = kumascript.get( - doc, base_url=None, selective_mode=(mode, select_macros)) + doc, base_url=None, selective_mode=(mode, select_macros) + ) else: html, _ = doc.html, [] response = HttpResponse(html) - response['X-Frame-Options'] = 'deny' - response['X-Robots-Tag'] = 'noindex' + response["X-Frame-Options"] = "deny" + response["X-Robots-Tag"] = "noindex" return response @@ -288,14 +295,14 @@ def do_revision_api_post(request, doc, data): # Create a new revision and make it the document's current revision. doc.revise(request.user, data) # Schedule an immediate re-rendering of the document. - doc.schedule_rendering(cache_control='max-age=0') + doc.schedule_rendering(cache_control="max-age=0") # Schedule event notifications. EditDocumentEvent(doc.current_revision).fire(exclude=request.user) response = HttpResponse(doc.html, status=201) - rev_url = f'{doc.get_absolute_url()}$revision/{doc.current_revision.id}' - response['Location'] = request.build_absolute_uri(rev_url) + rev_url = f"{doc.get_absolute_url()}$revision/{doc.current_revision.id}" + response["Location"] = request.build_absolute_uri(rev_url) # Set the "ETag" header or else the "etag" decorator will set it according # to the document's previous revision, i.e. the current revision prior to # the "revise" method call above. - response['ETag'] = f'"{str(doc.current_revision.id)}"' + response["ETag"] = f'"{str(doc.current_revision.id)}"' return response diff --git a/kuma/wiki/views/translate.py b/kuma/wiki/views/translate.py index d77c56e2bb4..c1daffa2624 100644 --- a/kuma/wiki/views/translate.py +++ b/kuma/wiki/views/translate.py @@ -11,15 +11,13 @@ import kuma.wiki.content from kuma.attachments.forms import AttachmentRevisionForm -from kuma.core.decorators import (block_user_agents, ensure_wiki_domain, - login_required) +from kuma.core.decorators import block_user_agents, ensure_wiki_domain, login_required from kuma.core.i18n import get_language_mapping from kuma.core.urlresolvers import reverse from kuma.core.utils import get_object_or_none, smart_int, urlparams from .utils import document_form_initial, split_slug -from ..decorators import (check_readonly, prevent_indexing, - process_document_path) +from ..decorators import check_readonly, prevent_indexing, process_document_path from ..forms import DocumentForm, RevisionForm from ..models import Document, Revision @@ -33,10 +31,8 @@ def select_locale(request, document_slug, document_locale): """ Select a locale to translate the document to. """ - doc = get_object_or_404(Document, - locale=document_locale, - slug=document_slug) - return render(request, 'wiki/select_locale.html', {'document': doc}) + doc = get_object_or_404(Document, locale=document_locale, slug=document_slug) + return render(request, "wiki/select_locale.html", {"document": doc}) @ensure_wiki_domain @@ -58,9 +54,9 @@ def translate(request, document_slug, document_locale): # That might help reduce the headache-inducing branchiness. # The parent document to translate from - parent_doc = get_object_or_404(Document, - locale=settings.WIKI_DEFAULT_LANGUAGE, - slug=document_slug) + parent_doc = get_object_or_404( + Document, locale=settings.WIKI_DEFAULT_LANGUAGE, slug=document_slug + ) # Get the mapping here and now so it can be used for input validation language_mapping = get_language_mapping() @@ -68,7 +64,7 @@ def translate(request, document_slug, document_locale): # HACK: Seems weird, but sticking the translate-to locale in a query # param is the best way to avoid the MindTouch-legacy locale # redirection logic. - document_locale = request.GET.get('tolocale', document_locale) + document_locale = request.GET.get("tolocale", document_locale) if document_locale.lower() not in language_mapping: # The 'tolocale' query string parameters aren't free-text. They're # explicitly listed on the "Select language" page (`...$locales`) @@ -76,22 +72,26 @@ def translate(request, document_slug, document_locale): raise Http404 # Set a "Discard Changes" page - discard_href = '' + discard_href = "" if settings.WIKI_DEFAULT_LANGUAGE == document_locale: # Don't translate to the default language. - return redirect(reverse( - 'wiki.edit', locale=settings.WIKI_DEFAULT_LANGUAGE, - args=[parent_doc.slug])) + return redirect( + reverse( + "wiki.edit", + locale=settings.WIKI_DEFAULT_LANGUAGE, + args=[parent_doc.slug], + ) + ) if not parent_doc.is_localizable: - message = _('You cannot translate this document.') - context = {'message': message} - return render(request, 'handlers/400.html', context, status=400) + message = _("You cannot translate this document.") + context = {"message": message} + return render(request, "handlers/400.html", context, status=400) based_on_rev = parent_doc.current_or_latest_revision() - disclose_description = bool(request.GET.get('opendescription')) + disclose_description = bool(request.GET.get("opendescription")) try: doc = parent_doc.translations.get(locale=document_locale) @@ -104,12 +104,12 @@ def translate(request, document_slug, document_locale): # Find the "real" parent topic, which is its translation if parent_doc.parent_topic: try: - parent_topic_translated_doc = (parent_doc.parent_topic - .translations - .get(locale=document_locale)) - slug_dict = split_slug(parent_topic_translated_doc.slug + - '/' + - slug_dict['specific']) + parent_topic_translated_doc = parent_doc.parent_topic.translations.get( + locale=document_locale + ) + slug_dict = split_slug( + parent_topic_translated_doc.slug + "/" + slug_dict["specific"] + ) except ObjectDoesNotExist: pass @@ -120,22 +120,20 @@ def translate(request, document_slug, document_locale): if doc: # If there's an existing doc, populate form from it. discard_href = doc.get_absolute_url() - doc.slug = slug_dict['specific'] + doc.slug = slug_dict["specific"] doc_initial = document_form_initial(doc) else: # If no existing doc, bring over the original title and slug. discard_href = parent_doc.get_absolute_url() - doc_initial = {'title': based_on_rev.title, - 'slug': slug_dict['specific']} - doc_form = DocumentForm(initial=doc_initial, - parent_slug=slug_dict['parent']) + doc_initial = {"title": based_on_rev.title, "slug": slug_dict["specific"]} + doc_form = DocumentForm(initial=doc_initial, parent_slug=slug_dict["parent"]) initial = { - 'based_on': based_on_rev.id, - 'current_rev': doc.current_or_latest_revision().id if doc else None, - 'comment': '', - 'toc_depth': based_on_rev.toc_depth, - 'localization_tags': ['inprogress'], + "based_on": based_on_rev.id, + "current_rev": doc.current_or_latest_revision().id if doc else None, + "comment": "", + "toc_depth": based_on_rev.toc_depth, + "localization_tags": ["inprogress"], } content = None if not doc: @@ -145,71 +143,73 @@ def translate(request, document_slug, document_locale): # that calls "clean_content" on Revision.save is deployed to # production, AND the current revisions of all docs have had # their content cleaned with "clean_content". - initial.update(content=kuma.wiki.content.parse(content) - .filterEditorSafety() - .serialize()) + initial.update( + content=kuma.wiki.content.parse(content).filterEditorSafety().serialize() + ) instance = doc and doc.current_or_latest_revision() - rev_form = RevisionForm(request=request, - instance=instance, - initial=initial, - parent_slug=slug_dict['parent']) - - if request.method == 'POST': - which_form = request.POST.get('form-type', 'both') + rev_form = RevisionForm( + request=request, + instance=instance, + initial=initial, + parent_slug=slug_dict["parent"], + ) + + if request.method == "POST": + which_form = request.POST.get("form-type", "both") doc_form_invalid = False # Grab the posted slug value in case it's invalid - posted_slug = request.POST.get('slug', slug_dict['specific']) + posted_slug = request.POST.get("slug", slug_dict["specific"]) - if user_has_doc_perm and which_form in ['doc', 'both']: + if user_has_doc_perm and which_form in ["doc", "both"]: disclose_description = True post_data = request.POST.copy() - post_data.update({'locale': document_locale}) + post_data.update({"locale": document_locale}) - doc_form = DocumentForm(post_data, instance=doc, - parent_slug=slug_dict['parent']) + doc_form = DocumentForm( + post_data, instance=doc, parent_slug=slug_dict["parent"] + ) doc_form.instance.locale = document_locale doc_form.instance.parent = parent_doc - if which_form == 'both': + if which_form == "both": # Sending a new copy of post so the slug change above # doesn't cause problems during validation - rev_form = RevisionForm(request=request, - data=post_data, - parent_slug=slug_dict['parent']) + rev_form = RevisionForm( + request=request, data=post_data, parent_slug=slug_dict["parent"] + ) # If we are submitting the whole form, we need to check that # the Revision is valid before saving the Document. - if doc_form.is_valid() and (which_form == 'doc' or - rev_form.is_valid()): + if doc_form.is_valid() and (which_form == "doc" or rev_form.is_valid()): doc = doc_form.save(parent=parent_doc) - if which_form == 'doc': + if which_form == "doc": url = urlparams(doc.get_edit_url(), opendescription=1) return redirect(url) else: - doc_form.data['slug'] = posted_slug + doc_form.data["slug"] = posted_slug doc_form_invalid = True - if doc and which_form in ['rev', 'both']: + if doc and which_form in ["rev", "both"]: post_data = request.POST.copy() - if 'slug' not in post_data: - post_data['slug'] = posted_slug + if "slug" not in post_data: + post_data["slug"] = posted_slug # update the post data with the toc_depth of original - post_data['toc_depth'] = based_on_rev.toc_depth + post_data["toc_depth"] = based_on_rev.toc_depth # Pass in the locale for the akistmet "blog_lang". - post_data['locale'] = document_locale + post_data["locale"] = document_locale - rev_form = RevisionForm(request=request, - data=post_data, - parent_slug=slug_dict['parent']) + rev_form = RevisionForm( + request=request, data=post_data, parent_slug=slug_dict["parent"] + ) rev_form.instance.document = doc # for rev_form.clean() if rev_form.is_valid() and not doc_form_invalid: - parent_id = request.POST.get('parent_id', '') + parent_id = request.POST.get("parent_id", "") # Attempt to set a parent if parent_id: @@ -225,8 +225,8 @@ def translate(request, document_slug, document_locale): # If this is an Ajax POST, then return a JsonResponse if request.is_ajax(): data = { - 'error': False, - 'new_revision_id': rev_form.instance.id, + "error": False, + "new_revision_id": rev_form.instance.id, } return JsonResponse(data) @@ -235,17 +235,18 @@ def translate(request, document_slug, document_locale): url = doc.get_absolute_url() params = {} # Parameter for the document saved, so that we can delete the cached draft on load - params['rev_saved'] = request.POST.get('current_rev', '') - url = '%s?%s' % (url, urlencode(params)) + params["rev_saved"] = request.POST.get("current_rev", "") + url = "%s?%s" % (url, urlencode(params)) return redirect(url) else: # If this is an Ajax POST, then return a JsonResponse with error if request.is_ajax(): - if 'current_rev' in rev_form._errors: + if "current_rev" in rev_form._errors: # Make the error message safe so the '<' and '>' don't # get turned into '<' and '>', respectively - rev_form.errors['current_rev'][0] = mark_safe( - rev_form.errors['current_rev'][0]) + rev_form.errors["current_rev"][0] = mark_safe( + rev_form.errors["current_rev"][0] + ) errors = [rev_form.errors[key][0] for key in rev_form.errors.keys()] data = { "error": True, @@ -255,15 +256,11 @@ def translate(request, document_slug, document_locale): return JsonResponse(data=data) if doc: - from_id = smart_int(request.GET.get('from'), None) - to_id = smart_int(request.GET.get('to'), None) - - revision_from = get_object_or_none(Revision, - pk=from_id, - document=doc.parent) - revision_to = get_object_or_none(Revision, - pk=to_id, - document=doc.parent) + from_id = smart_int(request.GET.get("from"), None) + to_id = smart_int(request.GET.get("to"), None) + + revision_from = get_object_or_none(Revision, pk=from_id, document=doc.parent) + revision_to = get_object_or_none(Revision, pk=to_id, document=doc.parent) else: revision_from = revision_to = None @@ -273,20 +270,20 @@ def translate(request, document_slug, document_locale): default_locale = language_mapping[settings.WIKI_DEFAULT_LANGUAGE.lower()] context = { - 'parent': parent_doc, - 'document': doc, - 'document_form': doc_form, - 'revision_form': rev_form, - 'locale': document_locale, - 'default_locale': default_locale, - 'language': language, - 'based_on': based_on_rev, - 'disclose_description': disclose_description, - 'discard_href': discard_href, - 'attachment_form': AttachmentRevisionForm(), - 'specific_slug': parent_split['specific'], - 'parent_slug': parent_split['parent'], - 'revision_from': revision_from, - 'revision_to': revision_to, + "parent": parent_doc, + "document": doc, + "document_form": doc_form, + "revision_form": rev_form, + "locale": document_locale, + "default_locale": default_locale, + "language": language, + "based_on": based_on_rev, + "disclose_description": disclose_description, + "discard_href": discard_href, + "attachment_form": AttachmentRevisionForm(), + "specific_slug": parent_split["specific"], + "parent_slug": parent_split["parent"], + "revision_from": revision_from, + "revision_to": revision_to, } - return render(request, 'wiki/translate.html', context) + return render(request, "wiki/translate.html", context) diff --git a/kuma/wiki/views/utils.py b/kuma/wiki/views/utils.py index 527876207b1..ade03bba35c 100644 --- a/kuma/wiki/views/utils.py +++ b/kuma/wiki/views/utils.py @@ -1,5 +1,3 @@ - - import hashlib @@ -7,34 +5,34 @@ def split_slug(slug): """ Utility function to do basic slug splitting """ - slug_split = slug.split('/') + slug_split = slug.split("/") length = len(slug_split) root = None - seo_root = '' - bad_seo_roots = ['Web'] + seo_root = "" + bad_seo_roots = ["Web"] if length > 1: root = slug_split[0] if root in bad_seo_roots: if length > 2: - seo_root = root + '/' + slug_split[1] + seo_root = root + "/" + slug_split[1] else: seo_root = root specific = slug_split.pop() - parent = '/'.join(slug_split) + parent = "/".join(slug_split) - return { # with this given: "some/kind/of/Path" - 'specific': specific, # 'Path' - 'parent': parent, # 'some/kind/of' - 'full': slug, # 'some/kind/of/Path' - 'parent_split': slug_split, # ['some', 'kind', 'of'] - 'length': length, # 4 - 'root': root, # 'some' - 'seo_root': seo_root, # 'some' + return { # with this given: "some/kind/of/Path" + "specific": specific, # 'Path' + "parent": parent, # 'some/kind/of' + "full": slug, # 'some/kind/of/Path' + "parent_split": slug_split, # ['some', 'kind', 'of'] + "length": length, # 4 + "root": root, # 'some' + "seo_root": seo_root, # 'some' } @@ -43,10 +41,10 @@ def document_form_initial(document): Return a dict with the document data pertinent for the form. """ return { - 'title': document.title, - 'slug': document.slug, - 'is_localizable': document.is_localizable, - 'tags': list(document.tags.names()) + "title": document.title, + "slug": document.slug, + "is_localizable": document.is_localizable, + "tags": list(document.tags.names()), } diff --git a/kuma/wsgi.py b/kuma/wsgi.py index 00caa197d8f..5df29917a85 100644 --- a/kuma/wsgi.py +++ b/kuma/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'kuma.settings.local') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "kuma.settings.local") application = get_wsgi_application() diff --git a/tests/conftest.py b/tests/conftest.py index 2f7002587af..7229feb0e1e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,38 +26,37 @@ def pytest_configure(config): # The pytest-base-url plugin adds --base-url, and sets the default from # environment variable PYTEST_BASE_URL. If still unset, force to staging. if config.option.base_url is None: - config.option.base_url = 'https://developer.allizom.org' - base_url = config.getoption('base_url') + config.option.base_url = "https://developer.allizom.org" + base_url = config.getoption("base_url") # Process the server status from _kuma_status.json base_parts = urlsplit(base_url) - kuma_status_url = urlunsplit((base_parts.scheme, base_parts.netloc, - '_kuma_status.json', '', '')) + kuma_status_url = urlunsplit( + (base_parts.scheme, base_parts.netloc, "_kuma_status.json", "", "") + ) session = requests.Session() - retries = Retry(total=4, backoff_factor=0.1, - status_forcelist=[500, 502, 503, 504]) + retries = Retry(total=4, backoff_factor=0.1, status_forcelist=[500, 502, 503, 504]) session.mount(kuma_status_url, HTTPAdapter(max_retries=retries)) - response = session.get(kuma_status_url, - headers={'Accept': 'application/json'}) + response = session.get(kuma_status_url, headers={"Accept": "application/json"}) response.raise_for_status() _KUMA_STATUS = response.json() - _KUMA_STATUS['response'] = {'headers': response.headers} - config._metadata['kuma'] = _KUMA_STATUS + _KUMA_STATUS["response"] = {"headers": response.headers} + config._metadata["kuma"] = _KUMA_STATUS # Process the settings for this Kuma instance - settings = _KUMA_STATUS['settings'] - allowed_hosts = set(settings['ALLOWED_HOSTS']) + settings = _KUMA_STATUS["settings"] + allowed_hosts = set(settings["ALLOWED_HOSTS"]) host_urls = {base_url} - protocol = settings['PROTOCOL'] + protocol = settings["PROTOCOL"] for host in allowed_hosts: - if host != '*': + if host != "*": host_urls.add(protocol + host) # Setup dynamic fixtures _DYNAMIC_FIXTURES = { - 'any_host_url': { - 'argvalues': sorted(host_urls), - 'ids': [urlsplit(url).netloc for url in sorted(host_urls)] + "any_host_url": { + "argvalues": sorted(host_urls), + "ids": [urlsplit(url).netloc for url in sorted(host_urls)], } } @@ -77,47 +76,46 @@ def pytest_generate_tests(metafunc): metafunc.parametrize(name, params) -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def is_local_url(base_url): """ Returns True if the system-under-test is the local development instance (localhost). """ - return (base_url and - 'localhost' in urlsplit(base_url).hostname.split('.')) + return base_url and "localhost" in urlsplit(base_url).hostname.split(".") -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def kuma_status(base_url): return _KUMA_STATUS -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def is_debug(kuma_status): - return kuma_status['settings']['DEBUG'] + return kuma_status["settings"]["DEBUG"] -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def is_searchable(kuma_status): - search = kuma_status['services']['search'] - return search['available'] and search['populated'] + search = kuma_status["services"]["search"] + return search["available"] and search["populated"] -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def is_maintenance_mode(kuma_status): - return kuma_status['settings']['MAINTENANCE_MODE'] + return kuma_status["settings"]["MAINTENANCE_MODE"] -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def is_behind_cdn(kuma_status): - return 'x-amz-cf-id' in kuma_status['response']['headers'] + return "x-amz-cf-id" in kuma_status["response"]["headers"] -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def site_url(kuma_status): - return kuma_status['settings']['SITE_URL'] + return kuma_status["settings"]["SITE_URL"] -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def wiki_site_url(kuma_status): - return kuma_status['settings']['WIKI_SITE_URL'] + return kuma_status["settings"]["WIKI_SITE_URL"] diff --git a/tests/headless/__init__.py b/tests/headless/__init__.py index 65886e413b2..cf7eeb70d34 100644 --- a/tests/headless/__init__.py +++ b/tests/headless/__init__.py @@ -4,26 +4,25 @@ # Use pytest verbose asserts # https://stackoverflow.com/questions/41522767/pytest-assert-introspection-in-helper-function -pytest.register_assert_rewrite('utils.urls') +pytest.register_assert_rewrite("utils.urls") DEFAULT_TIMEOUT = 120 # seconds # Untrusted attachments and samples domains that are indexed INDEXED_ATTACHMENT_DOMAINS = { - 'mdn.mozillademos.org', # Main attachments domain - 'demos.mdn.mozit.cloud', # Alternate attachments domain (testing) - 'demos-origin.mdn.mozit.cloud', # Attachments origin + "mdn.mozillademos.org", # Main attachments domain + "demos.mdn.mozit.cloud", # Alternate attachments domain (testing) + "demos-origin.mdn.mozit.cloud", # Attachments origin } # Kuma web domains that are indexed -INDEXED_WEB_DOMAINS = { - 'developer.mozilla.org'} +INDEXED_WEB_DOMAINS = {"developer.mozilla.org"} def request(method, url, **kwargs): - if 'timeout' not in kwargs: + if "timeout" not in kwargs: kwargs.update(timeout=DEFAULT_TIMEOUT) - if 'allow_redirects' not in kwargs: + if "allow_redirects" not in kwargs: kwargs.update(allow_redirects=False) return requests.request(method, url, **kwargs) diff --git a/tests/headless/map_301.py b/tests/headless/map_301.py index 08899c03b0f..c81554018b4 100644 --- a/tests/headless/map_301.py +++ b/tests/headless/map_301.py @@ -4,566 +4,931 @@ # Converted from SCL3 Apache files -SCL3_REDIRECT_URLS = list(flatten(( - url_test("/media/redesign/css/foo-min.css", - "/static/build/styles/foo.css"), - url_test("/media/css/foo-min.css", "/static/build/styles/foo.css"), - - url_test("/media/redesign/js/foo-min.js", "/static/build/js/foo.js"), - url_test("/media/js/foo-min.js", "/static/build/js/foo.js"), - - url_test("/media/redesign/img.foo", "/static/img.foo"), - url_test("/media/img.foo", "/static/img.foo"), - - url_test("/media/redesign/css.foo", "/static/styles.foo"), - url_test("/media/css.foo", "/static/styles.foo"), - - url_test("/media/redesign/js.foo", "/static/js.foo"), - url_test("/media/js.foo", "/static/js.foo"), - - url_test("/media/redesign/fonts.foo", "/static/fonts.foo"), - url_test("/media/fonts.foo", "/static/fonts.foo"), - - url_test("/media/uploads/demos/foobar123", - "/docs/Web/Demos_of_open_web_technologies/", - status_code=requests.codes.found), - - url_test("/docs/Mozilla/Projects/NSPR/Reference/I//O_Functions", - "/docs/Mozilla/Projects/NSPR/Reference/I_O_Functions"), - url_test("/docs/Mozilla/Projects/NSPR/Reference/I//O//Functions", - "/docs/Mozilla/Projects/NSPR/Reference/I_O_Functions"), - - url_test("/samples/canvas-tutorial/2_1_canvas_rect.html", - "/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes#Rectangular_shape_example"), - url_test("/samples/canvas-tutorial/2_2_canvas_moveto.html", - "/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes#Moving_the_pen"), - url_test("/samples/canvas-tutorial/2_3_canvas_lineto.html", - "/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes#Lines"), - url_test("/samples/canvas-tutorial/2_4_canvas_arc.html", - "/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes#Arcs"), - url_test("/samples/canvas-tutorial/2_5_canvas_quadraticcurveto.html", - "/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes#Quadratic_Bezier_curves"), - url_test("/samples/canvas-tutorial/2_6_canvas_beziercurveto.html", - "/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes#Cubic_Bezier_curves"), - url_test("/samples/canvas-tutorial/3_1_canvas_drawimage.html", - "/docs/Web/API/Canvas_API/Tutorial/Using_images#Drawing_images"), - url_test("/samples/canvas-tutorial/3_2_canvas_drawimage.html", - "/docs/Web/API/Canvas_API/Tutorial/Using_images#Example.3A_Tiling_an_image"), - url_test("/samples/canvas-tutorial/3_3_canvas_drawimage.html", - "/docs/Web/API/Canvas_API/Tutorial/Using_images#Example.3A_Framing_an_image"), - url_test("/samples/canvas-tutorial/3_4_canvas_gallery.html", - "/docs/Web/API/Canvas_API/Tutorial/Using_images#Art_gallery_example"), - url_test("/samples/canvas-tutorial/4_1_canvas_fillstyle.html", - "/docs/Web/API/CanvasRenderingContext2D.fillStyle"), - url_test("/samples/canvas-tutorial/4_2_canvas_strokestyle.html", - "/docs/Web/API/CanvasRenderingContext2D.strokeStyle"), - url_test("/samples/canvas-tutorial/4_3_canvas_globalalpha.html", - "/docs/Web/API/CanvasRenderingContext2D.globalAlpha"), - url_test("/samples/canvas-tutorial/4_4_canvas_rgba.html", - "/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors#An_example_using_rgba()"), - url_test("/samples/canvas-tutorial/4_5_canvas_linewidth.html", - "/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors#A_lineWidth_example"), - url_test("/samples/canvas-tutorial/4_6_canvas_linecap.html", - "/docs/Web/API/CanvasRenderingContext2D.lineCap"), - url_test("/samples/canvas-tutorial/4_7_canvas_linejoin.html", - "/docs/Web/API/CanvasRenderingContext2D.lineJoin"), - url_test("/samples/canvas-tutorial/4_8_canvas_miterlimit.html", - "/docs/Web/API/CanvasRenderingContext2D.miterLimit"), - url_test("/samples/canvas-tutorial/4_9_canvas_lineargradient.html", - "/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors#A_createLinearGradient_example"), - url_test("/samples/canvas-tutorial/4_10_canvas_radialgradient.html", - "/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors#A_createRadialGradient_example"), - url_test("/samples/canvas-tutorial/4_11_canvas_createpattern.html", - "/docs/Web/API/CanvasRenderingContext2D.createPattern"), - url_test("/samples/canvas-tutorial/5_1_canvas_savestate.html", - "/docs/Web/API/Canvas_API/Tutorial/Transformations#A_save_and_restore_canvas_state_example"), - url_test("/samples/canvas-tutorial/5_2_canvas_translate.html", - "/docs/Web/API/CanvasRenderingContext2D.translate"), - url_test("/samples/canvas-tutorial/5_3_canvas_rotate.html", - "/docs/Web/API/CanvasRenderingContext2D.rotate"), - url_test("/samples/canvas-tutorial/5_4_canvas_scale.html", - "/docs/Web/API/CanvasRenderingContext2D.scale"), - url_test("/samples/canvas-tutorial/6_1_canvas_composite.html", - "/docs/Web/API/CanvasRenderingContext2D.globalCompositeOperation"), - url_test("/samples/canvas-tutorial/6_2_canvas_clipping.html", - "/docs/Web/API/Canvas_API/Tutorial/Compositing#Clipping_paths"), - url_test("/samples/canvas-tutorial/globalCompositeOperation.html", - "/docs/Web/API/CanvasRenderingContext2D.globalCompositeOperation"), - - url_test("/samples/domref/mozGetAsFile.html", - "/docs/Web/API/HTMLCanvasElement.mozGetAsFile"), - - url_test("/Firefox_OS/Security", "/docs/Mozilla/Firefox_OS/Security"), - - url_test("/en-US/mobile", "/en-US/docs/Mozilla/Mobile"), - url_test("/en-US/mobile/", "/en-US/docs/Mozilla/Mobile"), - url_test("/en/mobile/", "/en/docs/Mozilla/Mobile"), - - url_test("/en-US/addons", "/en-US/Add-ons"), - url_test("/en-US/addons/", "/en-US/Add-ons"), - url_test("/en/addons/", "/en/Add-ons"), - - url_test("/en-US/mozilla", "/en-US/docs/Mozilla"), - url_test("/en-US/mozilla/", "/en-US/docs/Mozilla"), - url_test("/en/mozilla/", "/en/docs/Mozilla"), - - url_test("/en-US/web", "/en-US/docs/Web"), - url_test("/en-US/web/", "/en-US/docs/Web"), - url_test("/en/web/", "/en/docs/Web"), - - url_test("/en-US/learn/html5", "/en-US/docs/Web/Guide/HTML/HTML5"), - url_test("/en-US/learn/html5/", "/en-US/docs/Web/Guide/HTML/HTML5"), - url_test("/en/learn/html5/", "/en/docs/Web/Guide/HTML/HTML5"), - - url_test("/En/JavaScript/Reference/Objects/Array", - "/en-US/docs/JavaScript/Reference/Global_Objects/Array"), - url_test("/En/JavaScript/Reference/Objects", - "/en-US/docs/JavaScript/Reference/Global_Objects/Object"), - url_test("/En/Core_JavaScript_1.5_Reference/Objects/foo", - "/en-US/docs/JavaScript/Reference/Global_Objects/foo"), - url_test("/En/Core_JavaScript_1.5_Reference/foo", - "/en-US/docs/JavaScript/Reference/foo"), - - url_test("/en-US/HTML5", "/en-US/docs/HTML/HTML5"), - url_test("/es/HTML5", "/es/docs/HTML/HTML5"), - - url_test("/web-tech/2008/09/12/css-transforms", - "/docs/CSS/Using_CSS_transforms"), - - url_test("/en-US/docs", "/en-US/docs/Web"), - url_test("/es/docs/", "/es/docs/Web"), - - url_test("/en-US/devnews/index.php/feed.foo", - "https://blog.mozilla.org/feed/"), - url_test("/en-US/devnews/foo", "https://wiki.mozilla.org/Releases"), - - url_test("/en-US/learn/html", "/en-US/Learn/HTML"), - url_test("/en/learn/html", "/en/Learn/HTML"), - - url_test("/en-US/learn/css", "/en-US/Learn/CSS"), - url_test("/en/learn/css", "/en/Learn/CSS"), - - url_test("/en-US/learn/javascript", "/en-US/Learn/JavaScript"), - url_test("/en/learn/javascript", "/en/Learn/JavaScript"), - - url_test("/en-US/learn", "/en-US/Learn"), - url_test("/en/learn", "/en/Learn"), - - url_test("/en-US/demos/detail/bananabread", - "https://github.com/kripken/BananaBread/"), - url_test("/en/demos/detail/bananabread", - "https://github.com/kripken/BananaBread/"), - - url_test("/en-US/demos/detail/bananabread/launch", - "https://kripken.github.io/BananaBread/cube2/index.html"), - url_test("/en/demos/detail/bananabread/launch", - "https://kripken.github.io/BananaBread/cube2/index.html"), - - url_test("/en-US/demos", "/en-US/docs/Web/Demos_of_open_web_technologies"), - url_test("/en/demos", "/en/docs/Web/Demos_of_open_web_technologies"), -))) +SCL3_REDIRECT_URLS = list( + flatten( + ( + url_test("/media/redesign/css/foo-min.css", "/static/build/styles/foo.css"), + url_test("/media/css/foo-min.css", "/static/build/styles/foo.css"), + url_test("/media/redesign/js/foo-min.js", "/static/build/js/foo.js"), + url_test("/media/js/foo-min.js", "/static/build/js/foo.js"), + url_test("/media/redesign/img.foo", "/static/img.foo"), + url_test("/media/img.foo", "/static/img.foo"), + url_test("/media/redesign/css.foo", "/static/styles.foo"), + url_test("/media/css.foo", "/static/styles.foo"), + url_test("/media/redesign/js.foo", "/static/js.foo"), + url_test("/media/js.foo", "/static/js.foo"), + url_test("/media/redesign/fonts.foo", "/static/fonts.foo"), + url_test("/media/fonts.foo", "/static/fonts.foo"), + url_test( + "/media/uploads/demos/foobar123", + "/docs/Web/Demos_of_open_web_technologies/", + status_code=requests.codes.found, + ), + url_test( + "/docs/Mozilla/Projects/NSPR/Reference/I//O_Functions", + "/docs/Mozilla/Projects/NSPR/Reference/I_O_Functions", + ), + url_test( + "/docs/Mozilla/Projects/NSPR/Reference/I//O//Functions", + "/docs/Mozilla/Projects/NSPR/Reference/I_O_Functions", + ), + url_test( + "/samples/canvas-tutorial/2_1_canvas_rect.html", + "/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes#Rectangular_shape_example", + ), + url_test( + "/samples/canvas-tutorial/2_2_canvas_moveto.html", + "/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes#Moving_the_pen", + ), + url_test( + "/samples/canvas-tutorial/2_3_canvas_lineto.html", + "/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes#Lines", + ), + url_test( + "/samples/canvas-tutorial/2_4_canvas_arc.html", + "/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes#Arcs", + ), + url_test( + "/samples/canvas-tutorial/2_5_canvas_quadraticcurveto.html", + "/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes#Quadratic_Bezier_curves", + ), + url_test( + "/samples/canvas-tutorial/2_6_canvas_beziercurveto.html", + "/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes#Cubic_Bezier_curves", + ), + url_test( + "/samples/canvas-tutorial/3_1_canvas_drawimage.html", + "/docs/Web/API/Canvas_API/Tutorial/Using_images#Drawing_images", + ), + url_test( + "/samples/canvas-tutorial/3_2_canvas_drawimage.html", + "/docs/Web/API/Canvas_API/Tutorial/Using_images#Example.3A_Tiling_an_image", + ), + url_test( + "/samples/canvas-tutorial/3_3_canvas_drawimage.html", + "/docs/Web/API/Canvas_API/Tutorial/Using_images#Example.3A_Framing_an_image", + ), + url_test( + "/samples/canvas-tutorial/3_4_canvas_gallery.html", + "/docs/Web/API/Canvas_API/Tutorial/Using_images#Art_gallery_example", + ), + url_test( + "/samples/canvas-tutorial/4_1_canvas_fillstyle.html", + "/docs/Web/API/CanvasRenderingContext2D.fillStyle", + ), + url_test( + "/samples/canvas-tutorial/4_2_canvas_strokestyle.html", + "/docs/Web/API/CanvasRenderingContext2D.strokeStyle", + ), + url_test( + "/samples/canvas-tutorial/4_3_canvas_globalalpha.html", + "/docs/Web/API/CanvasRenderingContext2D.globalAlpha", + ), + url_test( + "/samples/canvas-tutorial/4_4_canvas_rgba.html", + "/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors#An_example_using_rgba()", + ), + url_test( + "/samples/canvas-tutorial/4_5_canvas_linewidth.html", + "/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors#A_lineWidth_example", + ), + url_test( + "/samples/canvas-tutorial/4_6_canvas_linecap.html", + "/docs/Web/API/CanvasRenderingContext2D.lineCap", + ), + url_test( + "/samples/canvas-tutorial/4_7_canvas_linejoin.html", + "/docs/Web/API/CanvasRenderingContext2D.lineJoin", + ), + url_test( + "/samples/canvas-tutorial/4_8_canvas_miterlimit.html", + "/docs/Web/API/CanvasRenderingContext2D.miterLimit", + ), + url_test( + "/samples/canvas-tutorial/4_9_canvas_lineargradient.html", + "/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors#A_createLinearGradient_example", + ), + url_test( + "/samples/canvas-tutorial/4_10_canvas_radialgradient.html", + "/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors#A_createRadialGradient_example", + ), + url_test( + "/samples/canvas-tutorial/4_11_canvas_createpattern.html", + "/docs/Web/API/CanvasRenderingContext2D.createPattern", + ), + url_test( + "/samples/canvas-tutorial/5_1_canvas_savestate.html", + "/docs/Web/API/Canvas_API/Tutorial/Transformations#A_save_and_restore_canvas_state_example", + ), + url_test( + "/samples/canvas-tutorial/5_2_canvas_translate.html", + "/docs/Web/API/CanvasRenderingContext2D.translate", + ), + url_test( + "/samples/canvas-tutorial/5_3_canvas_rotate.html", + "/docs/Web/API/CanvasRenderingContext2D.rotate", + ), + url_test( + "/samples/canvas-tutorial/5_4_canvas_scale.html", + "/docs/Web/API/CanvasRenderingContext2D.scale", + ), + url_test( + "/samples/canvas-tutorial/6_1_canvas_composite.html", + "/docs/Web/API/CanvasRenderingContext2D.globalCompositeOperation", + ), + url_test( + "/samples/canvas-tutorial/6_2_canvas_clipping.html", + "/docs/Web/API/Canvas_API/Tutorial/Compositing#Clipping_paths", + ), + url_test( + "/samples/canvas-tutorial/globalCompositeOperation.html", + "/docs/Web/API/CanvasRenderingContext2D.globalCompositeOperation", + ), + url_test( + "/samples/domref/mozGetAsFile.html", + "/docs/Web/API/HTMLCanvasElement.mozGetAsFile", + ), + url_test("/Firefox_OS/Security", "/docs/Mozilla/Firefox_OS/Security"), + url_test("/en-US/mobile", "/en-US/docs/Mozilla/Mobile"), + url_test("/en-US/mobile/", "/en-US/docs/Mozilla/Mobile"), + url_test("/en/mobile/", "/en/docs/Mozilla/Mobile"), + url_test("/en-US/addons", "/en-US/Add-ons"), + url_test("/en-US/addons/", "/en-US/Add-ons"), + url_test("/en/addons/", "/en/Add-ons"), + url_test("/en-US/mozilla", "/en-US/docs/Mozilla"), + url_test("/en-US/mozilla/", "/en-US/docs/Mozilla"), + url_test("/en/mozilla/", "/en/docs/Mozilla"), + url_test("/en-US/web", "/en-US/docs/Web"), + url_test("/en-US/web/", "/en-US/docs/Web"), + url_test("/en/web/", "/en/docs/Web"), + url_test("/en-US/learn/html5", "/en-US/docs/Web/Guide/HTML/HTML5"), + url_test("/en-US/learn/html5/", "/en-US/docs/Web/Guide/HTML/HTML5"), + url_test("/en/learn/html5/", "/en/docs/Web/Guide/HTML/HTML5"), + url_test( + "/En/JavaScript/Reference/Objects/Array", + "/en-US/docs/JavaScript/Reference/Global_Objects/Array", + ), + url_test( + "/En/JavaScript/Reference/Objects", + "/en-US/docs/JavaScript/Reference/Global_Objects/Object", + ), + url_test( + "/En/Core_JavaScript_1.5_Reference/Objects/foo", + "/en-US/docs/JavaScript/Reference/Global_Objects/foo", + ), + url_test( + "/En/Core_JavaScript_1.5_Reference/foo", + "/en-US/docs/JavaScript/Reference/foo", + ), + url_test("/en-US/HTML5", "/en-US/docs/HTML/HTML5"), + url_test("/es/HTML5", "/es/docs/HTML/HTML5"), + url_test( + "/web-tech/2008/09/12/css-transforms", "/docs/CSS/Using_CSS_transforms" + ), + url_test("/en-US/docs", "/en-US/docs/Web"), + url_test("/es/docs/", "/es/docs/Web"), + url_test( + "/en-US/devnews/index.php/feed.foo", "https://blog.mozilla.org/feed/" + ), + url_test("/en-US/devnews/foo", "https://wiki.mozilla.org/Releases"), + url_test("/en-US/learn/html", "/en-US/Learn/HTML"), + url_test("/en/learn/html", "/en/Learn/HTML"), + url_test("/en-US/learn/css", "/en-US/Learn/CSS"), + url_test("/en/learn/css", "/en/Learn/CSS"), + url_test("/en-US/learn/javascript", "/en-US/Learn/JavaScript"), + url_test("/en/learn/javascript", "/en/Learn/JavaScript"), + url_test("/en-US/learn", "/en-US/Learn"), + url_test("/en/learn", "/en/Learn"), + url_test( + "/en-US/demos/detail/bananabread", + "https://github.com/kripken/BananaBread/", + ), + url_test( + "/en/demos/detail/bananabread", + "https://github.com/kripken/BananaBread/", + ), + url_test( + "/en-US/demos/detail/bananabread/launch", + "https://kripken.github.io/BananaBread/cube2/index.html", + ), + url_test( + "/en/demos/detail/bananabread/launch", + "https://kripken.github.io/BananaBread/cube2/index.html", + ), + url_test("/en-US/demos", "/en-US/docs/Web/Demos_of_open_web_technologies"), + url_test("/en/demos", "/en/docs/Web/Demos_of_open_web_technologies"), + ) + ) +) # Converted from SCL3 Apache files - demos moved to GitHub -GITHUB_IO_URLS = list(flatten(( - # http://mdn.github.io - # canvas raycaster - url_test("/samples/raycaster/input.js", - "http://mdn.github.io/canvas-raycaster/input.js"), - url_test("/samples/raycaster/Level.js", - "http://mdn.github.io/canvas-raycaster/Level.js"), - url_test("/samples/raycaster/Player.js", - "http://mdn.github.io/canvas-raycaster/Player.js"), - url_test("/samples/raycaster/RayCaster.html", - "http://mdn.github.io/canvas-raycaster/index.html"), - url_test("/samples/raycaster/RayCaster.js", - "http://mdn.github.io/canvas-raycaster/RayCaster.js"), - url_test("/samples/raycaster/trace.css", - "http://mdn.github.io/canvas-raycaster/trace.css"), - url_test("/samples/raycaster/trace.js", - "http://mdn.github.io/canvas-raycaster/trace.js"), - - # Bug 1215255 - Redirect static WebGL examples - url_test("/samples/webgl/sample1", - "http://mdn.github.io/webgl-examples/tutorial/sample1"), - url_test("/samples/webgl/sample1/index.html", - "http://mdn.github.io/webgl-examples/tutorial/sample1/index.html"), - url_test("/samples/webgl/sample1/webgl-demo.js", - "http://mdn.github.io/webgl-examples/tutorial/sample1/webgl-demo.js"), - url_test("/samples/webgl/sample1/webgl.css", - "http://mdn.github.io/webgl-examples/tutorial/webgl.css"), - url_test("/samples/webgl/sample2", - "http://mdn.github.io/webgl-examples/tutorial/sample2"), - url_test("/samples/webgl/sample2/glUtils.js", - "http://mdn.github.io/webgl-examples/tutorial/glUtils.js"), - url_test("/samples/webgl/sample2/index.html", - "http://mdn.github.io/webgl-examples/tutorial/sample2/index.html"), - url_test("/samples/webgl/sample2/sylvester.js", - "http://mdn.github.io/webgl-examples/tutorial/sylvester.js"), - url_test("/samples/webgl/sample2/webgl-demo.js", - "http://mdn.github.io/webgl-examples/tutorial/sample2/webgl-demo.js"), - url_test("/samples/webgl/sample2/webgl.css", - "http://mdn.github.io/webgl-examples/tutorial/webgl.css"), - url_test("/samples/webgl/sample3", - "http://mdn.github.io/webgl-examples/tutorial/sample3"), - url_test("/samples/webgl/sample3/glUtils.js", - "http://mdn.github.io/webgl-examples/tutorial/glUtils.js"), - url_test("/samples/webgl/sample3/index.html", - "http://mdn.github.io/webgl-examples/tutorial/sample3/index.html"), - url_test("/samples/webgl/sample3/sylvester.js", - "http://mdn.github.io/webgl-examples/tutorial/sylvester.js"), - url_test("/samples/webgl/sample3/webgl-demo.js", - "http://mdn.github.io/webgl-examples/tutorial/sample3/webgl-demo.js"), - url_test("/samples/webgl/sample3/webgl.css", - "http://mdn.github.io/webgl-examples/tutorial/webgl.css"), - url_test("/samples/webgl/sample4", - "http://mdn.github.io/webgl-examples/tutorial/sample4"), - url_test("/samples/webgl/sample4/glUtils.js", - "http://mdn.github.io/webgl-examples/tutorial/glUtils.js"), - url_test("/samples/webgl/sample4/index.html", - "http://mdn.github.io/webgl-examples/tutorial/sample4/index.html"), - url_test("/samples/webgl/sample4/sylvester.js", - "http://mdn.github.io/webgl-examples/tutorial/sylvester.js"), - url_test("/samples/webgl/sample4/webgl-demo.js", - "http://mdn.github.io/webgl-examples/tutorial/sample4/webgl-demo.js"), - url_test("/samples/webgl/sample4/webgl.css", - "http://mdn.github.io/webgl-examples/tutorial/webgl.css"), - url_test("/samples/webgl/sample5", - "http://mdn.github.io/webgl-examples/tutorial/sample5"), - url_test("/samples/webgl/sample5/glUtils.js", - "http://mdn.github.io/webgl-examples/tutorial/glUtils.js"), - url_test("/samples/webgl/sample5/index.html", - "http://mdn.github.io/webgl-examples/tutorial/sample5/index.html"), - url_test("/samples/webgl/sample5/sylvester.js", - "http://mdn.github.io/webgl-examples/tutorial/sylvester.js"), - url_test("/samples/webgl/sample5/webgl-demo.js", - "http://mdn.github.io/webgl-examples/tutorial/sample5/webgl-demo.js"), - url_test("/samples/webgl/sample5/webgl.css", - "http://mdn.github.io/webgl-examples/tutorial/webgl.css"), - url_test("/samples/webgl/sample6", - "http://mdn.github.io/webgl-examples/tutorial/sample6"), - url_test("/samples/webgl/sample6/cubetexture.png", - "http://mdn.github.io/webgl-examples/tutorial/sample6/cubetexture.png"), - url_test("/samples/webgl/sample6/glUtils.js", - "http://mdn.github.io/webgl-examples/tutorial/glUtils.js"), - url_test("/samples/webgl/sample6/index.html", - "http://mdn.github.io/webgl-examples/tutorial/sample6/index.html"), - url_test("/samples/webgl/sample6/sylvester.js", - "http://mdn.github.io/webgl-examples/tutorial/sylvester.js"), - url_test("/samples/webgl/sample6/webgl-demo.js", - "http://mdn.github.io/webgl-examples/tutorial/sample6/webgl-demo.js"), - url_test("/samples/webgl/sample6/webgl.css", - "http://mdn.github.io/webgl-examples/tutorial/webgl.css"), - url_test("/samples/webgl/sample7", - "http://mdn.github.io/webgl-examples/tutorial/sample7"), - url_test("/samples/webgl/sample7/cubetexture.png", - "http://mdn.github.io/webgl-examples/tutorial/sample7/cubetexture.png"), - url_test("/samples/webgl/sample7/glUtils.js", - "http://mdn.github.io/webgl-examples/tutorial/glUtils.js"), - url_test("/samples/webgl/sample7/index.html", - "http://mdn.github.io/webgl-examples/tutorial/sample7/index.html"), - url_test("/samples/webgl/sample7/sylvester.js", - "http://mdn.github.io/webgl-examples/tutorial/sylvester.js"), - url_test("/samples/webgl/sample7/webgl-demo.js", - "http://mdn.github.io/webgl-examples/tutorial/sample7/webgl-demo.js"), - url_test("/samples/webgl/sample7/webgl.css", - "http://mdn.github.io/webgl-examples/tutorial/webgl.css"), - url_test("/samples/webgl/sample8", - "http://mdn.github.io/webgl-examples/tutorial/sample8"), - url_test("/samples/webgl/sample8/Firefox.ogv", - "http://mdn.github.io/webgl-examples/tutorial/sample8/Firefox.ogv"), - url_test("/samples/webgl/sample8/glUtils.js", - "http://mdn.github.io/webgl-examples/tutorial/glUtils.js"), - url_test("/samples/webgl/sample8/index.html", - "http://mdn.github.io/webgl-examples/tutorial/sample8/index.html"), - url_test("/samples/webgl/sample8/sylvester.js", - "http://mdn.github.io/webgl-examples/tutorial/sylvester.js"), - url_test("/samples/webgl/sample8/webgl-demo.js", - "http://mdn.github.io/webgl-examples/tutorial/sample8/webgl-demo.js"), - url_test("/samples/webgl/sample8/webgl.css", - "http://mdn.github.io/webgl-examples/tutorial/webgl.css"), -))) +GITHUB_IO_URLS = list( + flatten( + ( + # http://mdn.github.io + # canvas raycaster + url_test( + "/samples/raycaster/input.js", + "http://mdn.github.io/canvas-raycaster/input.js", + ), + url_test( + "/samples/raycaster/Level.js", + "http://mdn.github.io/canvas-raycaster/Level.js", + ), + url_test( + "/samples/raycaster/Player.js", + "http://mdn.github.io/canvas-raycaster/Player.js", + ), + url_test( + "/samples/raycaster/RayCaster.html", + "http://mdn.github.io/canvas-raycaster/index.html", + ), + url_test( + "/samples/raycaster/RayCaster.js", + "http://mdn.github.io/canvas-raycaster/RayCaster.js", + ), + url_test( + "/samples/raycaster/trace.css", + "http://mdn.github.io/canvas-raycaster/trace.css", + ), + url_test( + "/samples/raycaster/trace.js", + "http://mdn.github.io/canvas-raycaster/trace.js", + ), + # Bug 1215255 - Redirect static WebGL examples + url_test( + "/samples/webgl/sample1", + "http://mdn.github.io/webgl-examples/tutorial/sample1", + ), + url_test( + "/samples/webgl/sample1/index.html", + "http://mdn.github.io/webgl-examples/tutorial/sample1/index.html", + ), + url_test( + "/samples/webgl/sample1/webgl-demo.js", + "http://mdn.github.io/webgl-examples/tutorial/sample1/webgl-demo.js", + ), + url_test( + "/samples/webgl/sample1/webgl.css", + "http://mdn.github.io/webgl-examples/tutorial/webgl.css", + ), + url_test( + "/samples/webgl/sample2", + "http://mdn.github.io/webgl-examples/tutorial/sample2", + ), + url_test( + "/samples/webgl/sample2/glUtils.js", + "http://mdn.github.io/webgl-examples/tutorial/glUtils.js", + ), + url_test( + "/samples/webgl/sample2/index.html", + "http://mdn.github.io/webgl-examples/tutorial/sample2/index.html", + ), + url_test( + "/samples/webgl/sample2/sylvester.js", + "http://mdn.github.io/webgl-examples/tutorial/sylvester.js", + ), + url_test( + "/samples/webgl/sample2/webgl-demo.js", + "http://mdn.github.io/webgl-examples/tutorial/sample2/webgl-demo.js", + ), + url_test( + "/samples/webgl/sample2/webgl.css", + "http://mdn.github.io/webgl-examples/tutorial/webgl.css", + ), + url_test( + "/samples/webgl/sample3", + "http://mdn.github.io/webgl-examples/tutorial/sample3", + ), + url_test( + "/samples/webgl/sample3/glUtils.js", + "http://mdn.github.io/webgl-examples/tutorial/glUtils.js", + ), + url_test( + "/samples/webgl/sample3/index.html", + "http://mdn.github.io/webgl-examples/tutorial/sample3/index.html", + ), + url_test( + "/samples/webgl/sample3/sylvester.js", + "http://mdn.github.io/webgl-examples/tutorial/sylvester.js", + ), + url_test( + "/samples/webgl/sample3/webgl-demo.js", + "http://mdn.github.io/webgl-examples/tutorial/sample3/webgl-demo.js", + ), + url_test( + "/samples/webgl/sample3/webgl.css", + "http://mdn.github.io/webgl-examples/tutorial/webgl.css", + ), + url_test( + "/samples/webgl/sample4", + "http://mdn.github.io/webgl-examples/tutorial/sample4", + ), + url_test( + "/samples/webgl/sample4/glUtils.js", + "http://mdn.github.io/webgl-examples/tutorial/glUtils.js", + ), + url_test( + "/samples/webgl/sample4/index.html", + "http://mdn.github.io/webgl-examples/tutorial/sample4/index.html", + ), + url_test( + "/samples/webgl/sample4/sylvester.js", + "http://mdn.github.io/webgl-examples/tutorial/sylvester.js", + ), + url_test( + "/samples/webgl/sample4/webgl-demo.js", + "http://mdn.github.io/webgl-examples/tutorial/sample4/webgl-demo.js", + ), + url_test( + "/samples/webgl/sample4/webgl.css", + "http://mdn.github.io/webgl-examples/tutorial/webgl.css", + ), + url_test( + "/samples/webgl/sample5", + "http://mdn.github.io/webgl-examples/tutorial/sample5", + ), + url_test( + "/samples/webgl/sample5/glUtils.js", + "http://mdn.github.io/webgl-examples/tutorial/glUtils.js", + ), + url_test( + "/samples/webgl/sample5/index.html", + "http://mdn.github.io/webgl-examples/tutorial/sample5/index.html", + ), + url_test( + "/samples/webgl/sample5/sylvester.js", + "http://mdn.github.io/webgl-examples/tutorial/sylvester.js", + ), + url_test( + "/samples/webgl/sample5/webgl-demo.js", + "http://mdn.github.io/webgl-examples/tutorial/sample5/webgl-demo.js", + ), + url_test( + "/samples/webgl/sample5/webgl.css", + "http://mdn.github.io/webgl-examples/tutorial/webgl.css", + ), + url_test( + "/samples/webgl/sample6", + "http://mdn.github.io/webgl-examples/tutorial/sample6", + ), + url_test( + "/samples/webgl/sample6/cubetexture.png", + "http://mdn.github.io/webgl-examples/tutorial/sample6/cubetexture.png", + ), + url_test( + "/samples/webgl/sample6/glUtils.js", + "http://mdn.github.io/webgl-examples/tutorial/glUtils.js", + ), + url_test( + "/samples/webgl/sample6/index.html", + "http://mdn.github.io/webgl-examples/tutorial/sample6/index.html", + ), + url_test( + "/samples/webgl/sample6/sylvester.js", + "http://mdn.github.io/webgl-examples/tutorial/sylvester.js", + ), + url_test( + "/samples/webgl/sample6/webgl-demo.js", + "http://mdn.github.io/webgl-examples/tutorial/sample6/webgl-demo.js", + ), + url_test( + "/samples/webgl/sample6/webgl.css", + "http://mdn.github.io/webgl-examples/tutorial/webgl.css", + ), + url_test( + "/samples/webgl/sample7", + "http://mdn.github.io/webgl-examples/tutorial/sample7", + ), + url_test( + "/samples/webgl/sample7/cubetexture.png", + "http://mdn.github.io/webgl-examples/tutorial/sample7/cubetexture.png", + ), + url_test( + "/samples/webgl/sample7/glUtils.js", + "http://mdn.github.io/webgl-examples/tutorial/glUtils.js", + ), + url_test( + "/samples/webgl/sample7/index.html", + "http://mdn.github.io/webgl-examples/tutorial/sample7/index.html", + ), + url_test( + "/samples/webgl/sample7/sylvester.js", + "http://mdn.github.io/webgl-examples/tutorial/sylvester.js", + ), + url_test( + "/samples/webgl/sample7/webgl-demo.js", + "http://mdn.github.io/webgl-examples/tutorial/sample7/webgl-demo.js", + ), + url_test( + "/samples/webgl/sample7/webgl.css", + "http://mdn.github.io/webgl-examples/tutorial/webgl.css", + ), + url_test( + "/samples/webgl/sample8", + "http://mdn.github.io/webgl-examples/tutorial/sample8", + ), + url_test( + "/samples/webgl/sample8/Firefox.ogv", + "http://mdn.github.io/webgl-examples/tutorial/sample8/Firefox.ogv", + ), + url_test( + "/samples/webgl/sample8/glUtils.js", + "http://mdn.github.io/webgl-examples/tutorial/glUtils.js", + ), + url_test( + "/samples/webgl/sample8/index.html", + "http://mdn.github.io/webgl-examples/tutorial/sample8/index.html", + ), + url_test( + "/samples/webgl/sample8/sylvester.js", + "http://mdn.github.io/webgl-examples/tutorial/sylvester.js", + ), + url_test( + "/samples/webgl/sample8/webgl-demo.js", + "http://mdn.github.io/webgl-examples/tutorial/sample8/webgl-demo.js", + ), + url_test( + "/samples/webgl/sample8/webgl.css", + "http://mdn.github.io/webgl-examples/tutorial/webgl.css", + ), + ) + ) +) # Converted from SCL3 Apache files - move to untrusted domain -MOZILLADEMOS_URLS = list(flatten(( - # https://mdn.mozillademos.org/ - url_test("/samples/canvas-tutorial/images/backdrop.png", - "https://mdn.mozillademos.org/files/5395/backdrop.png"), - url_test("/samples/canvas-tutorial/images/bg_gallery.png", - "https://mdn.mozillademos.org/files/5415/bg_gallery.png"), - url_test("/samples/canvas-tutorial/images/gallery_1.jpg", - "https://mdn.mozillademos.org/files/5399/gallery_1.jpg"), - url_test("/samples/canvas-tutorial/images/gallery_2.jpg", - "https://mdn.mozillademos.org/files/5401/gallery_2.jpg"), - url_test("/samples/canvas-tutorial/images/gallery_3.jpg", - "https://mdn.mozillademos.org/files/5403/gallery_3.jpg"), - url_test("/samples/canvas-tutorial/images/gallery_4.jpg", - "https://mdn.mozillademos.org/files/5405/gallery_4.jpg"), - url_test("/samples/canvas-tutorial/images/gallery_5.jpg", - "https://mdn.mozillademos.org/files/5407/gallery_5.jpg"), - url_test("/samples/canvas-tutorial/images/gallery_6.jpg", - "https://mdn.mozillademos.org/files/5409/gallery_6.jpg"), - url_test("/samples/canvas-tutorial/images/gallery_7.jpg", - "https://mdn.mozillademos.org/files/5411/gallery_7.jpg"), - url_test("/samples/canvas-tutorial/images/gallery_8.jpg", - "https://mdn.mozillademos.org/files/5413/gallery_8.jpg"), - url_test("/samples/canvas-tutorial/images/picture_frame.png", - "https://mdn.mozillademos.org/files/242/Canvas_picture_frame.png"), - url_test("/samples/canvas-tutorial/images/rhino.jpg", - "https://mdn.mozillademos.org/files/5397/rhino.jpg"), - url_test("/samples/canvas-tutorial/images/wallpaper.png", - "https://mdn.mozillademos.org/files/222/Canvas_createpattern.png"), -))) +MOZILLADEMOS_URLS = list( + flatten( + ( + # https://mdn.mozillademos.org/ + url_test( + "/samples/canvas-tutorial/images/backdrop.png", + "https://mdn.mozillademos.org/files/5395/backdrop.png", + ), + url_test( + "/samples/canvas-tutorial/images/bg_gallery.png", + "https://mdn.mozillademos.org/files/5415/bg_gallery.png", + ), + url_test( + "/samples/canvas-tutorial/images/gallery_1.jpg", + "https://mdn.mozillademos.org/files/5399/gallery_1.jpg", + ), + url_test( + "/samples/canvas-tutorial/images/gallery_2.jpg", + "https://mdn.mozillademos.org/files/5401/gallery_2.jpg", + ), + url_test( + "/samples/canvas-tutorial/images/gallery_3.jpg", + "https://mdn.mozillademos.org/files/5403/gallery_3.jpg", + ), + url_test( + "/samples/canvas-tutorial/images/gallery_4.jpg", + "https://mdn.mozillademos.org/files/5405/gallery_4.jpg", + ), + url_test( + "/samples/canvas-tutorial/images/gallery_5.jpg", + "https://mdn.mozillademos.org/files/5407/gallery_5.jpg", + ), + url_test( + "/samples/canvas-tutorial/images/gallery_6.jpg", + "https://mdn.mozillademos.org/files/5409/gallery_6.jpg", + ), + url_test( + "/samples/canvas-tutorial/images/gallery_7.jpg", + "https://mdn.mozillademos.org/files/5411/gallery_7.jpg", + ), + url_test( + "/samples/canvas-tutorial/images/gallery_8.jpg", + "https://mdn.mozillademos.org/files/5413/gallery_8.jpg", + ), + url_test( + "/samples/canvas-tutorial/images/picture_frame.png", + "https://mdn.mozillademos.org/files/242/Canvas_picture_frame.png", + ), + url_test( + "/samples/canvas-tutorial/images/rhino.jpg", + "https://mdn.mozillademos.org/files/5397/rhino.jpg", + ), + url_test( + "/samples/canvas-tutorial/images/wallpaper.png", + "https://mdn.mozillademos.org/files/222/Canvas_createpattern.png", + ), + ) + ) +) # Converted from SCL3 Apache files - MindTouch / old hosted files -LEGACY_URLS = list(flatten(( - # bug 1362438 - url_test('/index.php', status_code=404), - url_test('/index.php?title=Special:Recentchanges&feed=atom', - status_code=404), - url_test('/index.php?title=En/HTML/Canvas&revision=11', - status_code=404), - url_test('/index.php?title=En/HTML/Canvas&revision=11', - status_code=404), - url_test('/patches', status_code=404), - url_test('/patches/foo', status_code=404), - url_test('/web-tech', status_code=404), - url_test('/web-tech/feed/atom/', status_code=404), - url_test('/css/wiki.css', status_code=404), - url_test('/css/base.css', status_code=404), - url_test('/contests', 'http://www.mozillalabs.com/', status_code=302), - url_test('/contests/', 'http://www.mozillalabs.com/', status_code=302), - url_test('/contests/extendfirefox/faq.php', 'http://www.mozillalabs.com/', - status_code=302), - url_test('/es4', 'http://www.ecma-international.org/memento/TC39.htm', - status_code=302), - url_test('/es4/', 'http://www.ecma-international.org/memento/TC39.htm', - status_code=302), - url_test('/es4/proposals/slice_syntax.html', - 'http://www.ecma-international.org/memento/TC39.htm', - status_code=302), - # bug 962148 - url_test('/en/docs/Web/CSS/Attribute_selectors', - '/en-US/docs/Web/CSS/Attribute_selectors', status_code=302), - url_test('/en/docs/Web/CSS/Attribute_selectors', - '/en-US/docs/Web/CSS/Attribute_selectors', status_code=302), - url_test('/cn/docs/Talk:Kakurady', '/zh-CN/docs/Talk:Kakurady', - status_code=302), - url_test('/zh_cn/docs/Web/API/RTCPeerConnection/addTrack', - '/zh-CN/docs/Web/API/RTCPeerConnection/addTrack', - status_code=302), - url_test('/zh_tw/docs/AJAX', '/zh-TW/docs/AJAX', status_code=302), -))) +LEGACY_URLS = list( + flatten( + ( + # bug 1362438 + url_test("/index.php", status_code=404), + url_test( + "/index.php?title=Special:Recentchanges&feed=atom", status_code=404 + ), + url_test("/index.php?title=En/HTML/Canvas&revision=11", status_code=404), + url_test("/index.php?title=En/HTML/Canvas&revision=11", status_code=404), + url_test("/patches", status_code=404), + url_test("/patches/foo", status_code=404), + url_test("/web-tech", status_code=404), + url_test("/web-tech/feed/atom/", status_code=404), + url_test("/css/wiki.css", status_code=404), + url_test("/css/base.css", status_code=404), + url_test("/contests", "http://www.mozillalabs.com/", status_code=302), + url_test("/contests/", "http://www.mozillalabs.com/", status_code=302), + url_test( + "/contests/extendfirefox/faq.php", + "http://www.mozillalabs.com/", + status_code=302, + ), + url_test( + "/es4", + "http://www.ecma-international.org/memento/TC39.htm", + status_code=302, + ), + url_test( + "/es4/", + "http://www.ecma-international.org/memento/TC39.htm", + status_code=302, + ), + url_test( + "/es4/proposals/slice_syntax.html", + "http://www.ecma-international.org/memento/TC39.htm", + status_code=302, + ), + # bug 962148 + url_test( + "/en/docs/Web/CSS/Attribute_selectors", + "/en-US/docs/Web/CSS/Attribute_selectors", + status_code=302, + ), + url_test( + "/en/docs/Web/CSS/Attribute_selectors", + "/en-US/docs/Web/CSS/Attribute_selectors", + status_code=302, + ), + url_test( + "/cn/docs/Talk:Kakurady", "/zh-CN/docs/Talk:Kakurady", status_code=302 + ), + url_test( + "/zh_cn/docs/Web/API/RTCPeerConnection/addTrack", + "/zh-CN/docs/Web/API/RTCPeerConnection/addTrack", + status_code=302, + ), + url_test("/zh_tw/docs/AJAX", "/zh-TW/docs/AJAX", status_code=302), + ) + ) +) zone_redirects = ( - ('Add-ons', 'Mozilla/Add-ons', 'WebExtensions', ('ar', - 'bn', 'ca', - 'de', 'en-US', 'es', - 'fa', 'fr', 'hu', - 'id', 'it', 'ja', - 'ms', 'nl', 'pl', - 'pt-BR', 'pt-PT', - 'ru', 'sv-SE', 'th', - 'uk', 'vi', 'zh-CN', - 'zh-TW', None)), - ('Add-ons', 'Mozilla/Πρόσθετα', 'WebExtensions', ('el',)), - ('Add-ons', 'Mozilla/애드온들', 'WebExtensions', ('ko',)), - ('Add-ons', 'Mozilla/Eklentiler', 'WebExtensions', ('tr',)), - ('Firefox', 'Mozilla/Firefox', 'Privacy', ('ar', 'bm', - 'ca', 'de', - 'el', 'en-US', 'es', - 'fi', 'fr', - 'he', 'hi-IN', - 'hu', 'id', - 'it', 'ja', 'ko', - 'ms', 'my', - 'nl', 'pl', 'pt-BR', 'pt-PT', - 'ru', - 'sv-SE', 'th', - 'tr', 'vi', - 'zh-CN', 'zh-TW', - None)), - ('Firefox', 'Mozilla/ফায়ারফক্স', 'Privacy', ('bn',)), - ('Apps', 'Web/Apps', 'Tutorials', ('en-US', 'fa', 'fr', 'ja', 'th', - 'zh-CN', 'zh-TW', None)), - ('Apps', 'Web/Aplicaciones', 'Tutorials', ('es',)), - ('Apps', 'Apps', 'Tutorials', ('bn', 'de', 'it', 'ko', 'pt-BR', 'ru')), - ('Learn', 'Learn', 'JavaScript', ('ca', 'de', None)), - ('Apprendre', 'Apprendre', 'JavaScript', ('fr',)), - ('Marketplace', 'Mozilla/Marketplace', 'APIs', ('de', 'en-US', 'es', - 'fr', 'it', 'ja', - 'zh-CN', None)), - ('Marketplace', 'Mozilla/بازار', 'APIs', ('fa',)), + ( + "Add-ons", + "Mozilla/Add-ons", + "WebExtensions", + ( + "ar", + "bn", + "ca", + "de", + "en-US", + "es", + "fa", + "fr", + "hu", + "id", + "it", + "ja", + "ms", + "nl", + "pl", + "pt-BR", + "pt-PT", + "ru", + "sv-SE", + "th", + "uk", + "vi", + "zh-CN", + "zh-TW", + None, + ), + ), + ("Add-ons", "Mozilla/Πρόσθετα", "WebExtensions", ("el",)), + ("Add-ons", "Mozilla/애드온들", "WebExtensions", ("ko",)), + ("Add-ons", "Mozilla/Eklentiler", "WebExtensions", ("tr",)), + ( + "Firefox", + "Mozilla/Firefox", + "Privacy", + ( + "ar", + "bm", + "ca", + "de", + "el", + "en-US", + "es", + "fi", + "fr", + "he", + "hi-IN", + "hu", + "id", + "it", + "ja", + "ko", + "ms", + "my", + "nl", + "pl", + "pt-BR", + "pt-PT", + "ru", + "sv-SE", + "th", + "tr", + "vi", + "zh-CN", + "zh-TW", + None, + ), + ), + ("Firefox", "Mozilla/ফায়ারফক্স", "Privacy", ("bn",)), + ( + "Apps", + "Web/Apps", + "Tutorials", + ("en-US", "fa", "fr", "ja", "th", "zh-CN", "zh-TW", None), + ), + ("Apps", "Web/Aplicaciones", "Tutorials", ("es",)), + ("Apps", "Apps", "Tutorials", ("bn", "de", "it", "ko", "pt-BR", "ru")), + ("Learn", "Learn", "JavaScript", ("ca", "de", None)), + ("Apprendre", "Apprendre", "JavaScript", ("fr",)), + ( + "Marketplace", + "Mozilla/Marketplace", + "APIs", + ("de", "en-US", "es", "fr", "it", "ja", "zh-CN", None), + ), + ("Marketplace", "Mozilla/بازار", "APIs", ("fa",)), ) zone_url_test_kwargs = { - 'status_code': 302, - 'resp_headers': { - 'cache-control': 'max-age=0, public, s-maxage=604800' - } + "status_code": 302, + "resp_headers": {"cache-control": "max-age=0, public, s-maxage=604800"}, } ZONE_REDIRECT_URLS = [] for zone_root, wiki_slug, child_path, locales in zone_redirects: for locale in locales: - prefix = ('/' + locale) if locale else '' - redirect_path = prefix + '/docs/' + wiki_slug - paths = [prefix + '/' + zone_root] + prefix = ("/" + locale) if locale else "" + redirect_path = prefix + "/docs/" + wiki_slug + paths = [prefix + "/" + zone_root] # Test with a "docs" based path as well if it makes sense. if zone_root != wiki_slug: - paths.append(prefix + '/docs/' + zone_root) + paths.append(prefix + "/docs/" + zone_root) for path in paths: # The zone root without a trailing slash. ZONE_REDIRECT_URLS.append( - url_test(path, redirect_path, **zone_url_test_kwargs)) + url_test(path, redirect_path, **zone_url_test_kwargs) + ) # The zone root with a trailing slash. ZONE_REDIRECT_URLS.append( - url_test(path + '/', redirect_path, **zone_url_test_kwargs)) + url_test(path + "/", redirect_path, **zone_url_test_kwargs) + ) # A zone child page with query parameters. ZONE_REDIRECT_URLS.append( - url_test(path + '/' + child_path + '?raw¯os', - redirect_path + '/' + child_path + '?raw¯os', - **zone_url_test_kwargs)) + url_test( + path + "/" + child_path + "?raw¯os", + redirect_path + "/" + child_path + "?raw¯os", + **zone_url_test_kwargs + ) + ) # The zone root with $edit. ZONE_REDIRECT_URLS.append( - url_test(path + '$edit', redirect_path + '$edit', - **zone_url_test_kwargs)) + url_test( + path + "$edit", redirect_path + "$edit", **zone_url_test_kwargs + ) + ) # A zone path with curly braces {} ZONE_REDIRECT_URLS.append( - url_test(path + '/{test}', redirect_path + '/{test}', - **zone_url_test_kwargs)) + url_test( + path + "/{test}", redirect_path + "/{test}", **zone_url_test_kwargs + ) + ) # Redirects added after 2017 AWS move -REDIRECT_URLS = list(flatten(( - url_test('/en-US/fellowship', - '/en-US/docs/Archive/2015_MDN_Fellowship_Program'), -))) +REDIRECT_URLS = list( + flatten( + ( + url_test( + "/en-US/fellowship", "/en-US/docs/Archive/2015_MDN_Fellowship_Program" + ), + ) + ) +) -marionette_client_docs_url = ( - 'https://marionette-client.readthedocs.io/en/latest/') +marionette_client_docs_url = "https://marionette-client.readthedocs.io/en/latest/" marionette_docs_root_url = ( - 'https://firefox-source-docs.mozilla.org/testing/marionette/marionette/') -marionette_locales = '{/en-US,/fr,/ja,/pl,/pt-BR,/ru,/zh-CN,}' -marionette_base = marionette_locales + '/docs/Mozilla/QA/Marionette' -marionette_multi_base = marionette_locales + '/docs/{Mozilla/QA/,}Marionette' + "https://firefox-source-docs.mozilla.org/testing/marionette/marionette/" +) +marionette_locales = "{/en-US,/fr,/ja,/pl,/pt-BR,/ru,/zh-CN,}" +marionette_base = marionette_locales + "/docs/Mozilla/QA/Marionette" +marionette_multi_base = marionette_locales + "/docs/{Mozilla/QA/,}Marionette" marionette_python_tests = ( - '{MarionetteTestCase,Marionette_Python_Tests,Running_Tests,Tests}') + "{MarionetteTestCase,Marionette_Python_Tests,Running_Tests,Tests}" +) -MARIONETTE_URLS = list(flatten(( - url_test(marionette_multi_base, marionette_docs_root_url + 'index.html'), - url_test(marionette_multi_base + '/Builds', - marionette_docs_root_url + 'Building.html'), - url_test(marionette_multi_base + '/Client', marionette_client_docs_url), - url_test(marionette_multi_base + '/Developer_setup', - marionette_docs_root_url + 'Contributing.html'), - url_test(marionette_multi_base + '/' + marionette_python_tests, - marionette_docs_root_url + 'PythonTests.html'), - url_test(marionette_locales + '/docs/Marionette_Test_Runner', - marionette_docs_root_url + 'PythonTests.html'), - url_test(marionette_base + '/Marionette_Test_Runner', - marionette_docs_root_url + 'PythonTests.html'), - url_test(marionette_base + '/Protocol', - marionette_docs_root_url + 'Protocol.html'), - url_test(marionette_base + '/Python_Client', - marionette_client_docs_url), - url_test(marionette_base + '/WebDriver/status', - 'https://bugzilla.mozilla.org' - '/showdependencytree.cgi?id=721859&hide_resolved=1'), - url_test(marionette_locales + '/docs/Marionette/Debugging', - marionette_docs_root_url + 'Debugging.html'), -))) +MARIONETTE_URLS = list( + flatten( + ( + url_test(marionette_multi_base, marionette_docs_root_url + "index.html"), + url_test( + marionette_multi_base + "/Builds", + marionette_docs_root_url + "Building.html", + ), + url_test(marionette_multi_base + "/Client", marionette_client_docs_url), + url_test( + marionette_multi_base + "/Developer_setup", + marionette_docs_root_url + "Contributing.html", + ), + url_test( + marionette_multi_base + "/" + marionette_python_tests, + marionette_docs_root_url + "PythonTests.html", + ), + url_test( + marionette_locales + "/docs/Marionette_Test_Runner", + marionette_docs_root_url + "PythonTests.html", + ), + url_test( + marionette_base + "/Marionette_Test_Runner", + marionette_docs_root_url + "PythonTests.html", + ), + url_test( + marionette_base + "/Protocol", + marionette_docs_root_url + "Protocol.html", + ), + url_test(marionette_base + "/Python_Client", marionette_client_docs_url), + url_test( + marionette_base + "/WebDriver/status", + "https://bugzilla.mozilla.org" + "/showdependencytree.cgi?id=721859&hide_resolved=1", + ), + url_test( + marionette_locales + "/docs/Marionette/Debugging", + marionette_docs_root_url + "Debugging.html", + ), + ) + ) +) -WEBEXT_URLS = list(flatten( - url_test( - '{/en-US,/fr,}/docs/Mozilla/Add-ons/' + ao_path, - 'https://extensionworkshop.com/documentation/' + ew_path - ) for ao_path, ew_path in ( - ('WebExtensions/Security_best_practices', - 'develop/build-a-secure-extension/'), - ('WebExtensions/user_interface/Accessibility_guidelines', - 'develop/build-an-accessible-extension/'), - ('WebExtensions/onboarding_upboarding_offboarding_best_practices', - 'develop/onboard-upboard-offboard-users/'), - ('WebExtensions/Porting_a_Google_Chrome_extension', - 'develop/porting-a-google-chrome-extension/'), - ('WebExtensions/Porting_a_legacy_Firefox_add-on', - 'develop/porting-a-legacy-firefox-extension/'), - ('WebExtensions/Comparison_with_the_Add-on_SDK', - 'develop/comparison-with-the-add-on-sdk/'), - ('WebExtensions/Comparison_with_XUL_XPCOM_extensions', - 'develop/comparison-with-xul-xpcom-extensions/'), - ('WebExtensions/Differences_between_desktop_and_Android', - 'develop/differences-between-desktop-and-android-extensions/'), - ('WebExtensions/Development_Tools', - 'develop/browser-extension-development-tools/'), - ('WebExtensions/Choose_a_Firefox_version_for_web_extension_develop', - 'develop/choosing-a-firefox-version-for-extension-development/'), - ('WebExtensions/User_experience_best_practices', - 'develop/user-experience-best-practices/'), - ('WebExtensions/Prompt_users_for_data_and_privacy_consents', - 'develop/best-practices-for-collecting-user-data-consents/'), - ('WebExtensions/Temporary_Installation_in_Firefox', - 'develop/temporary-installation-in-firefox/'), - ('WebExtensions/Debugging', - 'develop/debugging/'), - ('WebExtensions/Testing_persistent_and_restart_features', - 'develop/testing-persistent-and-restart-features/'), - ('WebExtensions/Test_permission_requests', - 'develop/test-permission-requests/'), - ('WebExtensions/Developing_WebExtensions_for_Firefox_for_Android', - 'develop/developing-extensions-for-firefox-for-android/'), - ('WebExtensions/Getting_started_with_web-ext', - 'develop/getting-started-with-web-ext/'), - ('WebExtensions/web-ext_command_reference', - 'develop/web-ext-command-reference/'), - ('WebExtensions/WebExtensions_and_the_Add-on_ID', - 'develop/extensions-and-the-add-on-id/'), - ('WebExtensions/Request_the_right_permissions', - 'develop/request-the-right-permissions/'), - ('WebExtensions/Best_practices_for_updating_your_extension', - 'manage/best-practices-for-updating/'), - ('Updates', - 'manage/updating-your-extension/'), - ('WebExtensions/Distribution_options', - 'publish/signing-and-distribution-overview/'), - ('Themes/Using_the_AMO_theme_generator', - 'themes/using-the-amo-theme-generator/'), - ('WebExtensions/Developer_accounts', - 'publish/developer-accounts/'), - ('Distribution', - 'publish/signing-and-distribution-overview/#distributing-your-addon'), - ('WebExtensions/Package_your_extension_', - 'publish/package-your-extension/'), - ('Distribution/Submitting_an_add-on', - 'publish/submitting-an-add-on/'), - ('Source_Code_Submission', - 'publish/source-code-submission/'), - ('Distribution/Resources_for_publishers', - 'manage/resources-for-publishers/'), - ('Listing', - 'develop/create-an-appealing-listing/'), - ('Distribution/Make_money_from_browser_extensions', - 'publish/make-money-from-browser-extensions/'), - ('Distribution/Promoting_your_extension_or_theme', - 'publish/promoting-your-extension/'), - ('AMO/Policy/Reviews', - 'publish/add-on-policies/'), - ('AMO/Policy/Agreement', - 'publish/firefox-add-on-distribution-agreement/'), - ('Distribution/Retiring_your_extension', - 'manage/retiring-your-extension/'), - ('WebExtensions/Distribution_options/Sideloading_add-ons', - 'publish/distribute-sideloading/'), - ('WebExtensions/Distribution_options/Add-ons_for_desktop_apps', - 'publish/distribute-for-desktop-apps/'), - ('WebExtensions/Distribution_options/Add-ons_in_the_enterprise', - 'enterprise/'), - ('AMO/Blocking_Process', - 'publish/add-ons-blocking-process/'), - ('Third_Party_Library_Usage', - 'publish/third-party-library-usage/'), - ('WebExtensions/What_does_review_rejection_mean_to_users', - 'publish/what-does-review-rejection-mean-to-users/'), - ('AMO/Policy/Featured', - 'publish/recommended-extensions/'), +WEBEXT_URLS = list( + flatten( + url_test( + "{/en-US,/fr,}/docs/Mozilla/Add-ons/" + ao_path, + "https://extensionworkshop.com/documentation/" + ew_path, + ) + for ao_path, ew_path in ( + ( + "WebExtensions/Security_best_practices", + "develop/build-a-secure-extension/", + ), + ( + "WebExtensions/user_interface/Accessibility_guidelines", + "develop/build-an-accessible-extension/", + ), + ( + "WebExtensions/onboarding_upboarding_offboarding_best_practices", + "develop/onboard-upboard-offboard-users/", + ), + ( + "WebExtensions/Porting_a_Google_Chrome_extension", + "develop/porting-a-google-chrome-extension/", + ), + ( + "WebExtensions/Porting_a_legacy_Firefox_add-on", + "develop/porting-a-legacy-firefox-extension/", + ), + ( + "WebExtensions/Comparison_with_the_Add-on_SDK", + "develop/comparison-with-the-add-on-sdk/", + ), + ( + "WebExtensions/Comparison_with_XUL_XPCOM_extensions", + "develop/comparison-with-xul-xpcom-extensions/", + ), + ( + "WebExtensions/Differences_between_desktop_and_Android", + "develop/differences-between-desktop-and-android-extensions/", + ), + ( + "WebExtensions/Development_Tools", + "develop/browser-extension-development-tools/", + ), + ( + "WebExtensions/Choose_a_Firefox_version_for_web_extension_develop", + "develop/choosing-a-firefox-version-for-extension-development/", + ), + ( + "WebExtensions/User_experience_best_practices", + "develop/user-experience-best-practices/", + ), + ( + "WebExtensions/Prompt_users_for_data_and_privacy_consents", + "develop/best-practices-for-collecting-user-data-consents/", + ), + ( + "WebExtensions/Temporary_Installation_in_Firefox", + "develop/temporary-installation-in-firefox/", + ), + ("WebExtensions/Debugging", "develop/debugging/"), + ( + "WebExtensions/Testing_persistent_and_restart_features", + "develop/testing-persistent-and-restart-features/", + ), + ( + "WebExtensions/Test_permission_requests", + "develop/test-permission-requests/", + ), + ( + "WebExtensions/Developing_WebExtensions_for_Firefox_for_Android", + "develop/developing-extensions-for-firefox-for-android/", + ), + ( + "WebExtensions/Getting_started_with_web-ext", + "develop/getting-started-with-web-ext/", + ), + ( + "WebExtensions/web-ext_command_reference", + "develop/web-ext-command-reference/", + ), + ( + "WebExtensions/WebExtensions_and_the_Add-on_ID", + "develop/extensions-and-the-add-on-id/", + ), + ( + "WebExtensions/Request_the_right_permissions", + "develop/request-the-right-permissions/", + ), + ( + "WebExtensions/Best_practices_for_updating_your_extension", + "manage/best-practices-for-updating/", + ), + ("Updates", "manage/updating-your-extension/"), + ( + "WebExtensions/Distribution_options", + "publish/signing-and-distribution-overview/", + ), + ( + "Themes/Using_the_AMO_theme_generator", + "themes/using-the-amo-theme-generator/", + ), + ("WebExtensions/Developer_accounts", "publish/developer-accounts/"), + ( + "Distribution", + "publish/signing-and-distribution-overview/#distributing-your-addon", + ), + ( + "WebExtensions/Package_your_extension_", + "publish/package-your-extension/", + ), + ("Distribution/Submitting_an_add-on", "publish/submitting-an-add-on/"), + ("Source_Code_Submission", "publish/source-code-submission/"), + ( + "Distribution/Resources_for_publishers", + "manage/resources-for-publishers/", + ), + ("Listing", "develop/create-an-appealing-listing/"), + ( + "Distribution/Make_money_from_browser_extensions", + "publish/make-money-from-browser-extensions/", + ), + ( + "Distribution/Promoting_your_extension_or_theme", + "publish/promoting-your-extension/", + ), + ("AMO/Policy/Reviews", "publish/add-on-policies/"), + ("AMO/Policy/Agreement", "publish/firefox-add-on-distribution-agreement/"), + ("Distribution/Retiring_your_extension", "manage/retiring-your-extension/"), + ( + "WebExtensions/Distribution_options/Sideloading_add-ons", + "publish/distribute-sideloading/", + ), + ( + "WebExtensions/Distribution_options/Add-ons_for_desktop_apps", + "publish/distribute-for-desktop-apps/", + ), + ( + "WebExtensions/Distribution_options/Add-ons_in_the_enterprise", + "enterprise/", + ), + ("AMO/Blocking_Process", "publish/add-ons-blocking-process/"), + ("Third_Party_Library_Usage", "publish/third-party-library-usage/"), + ( + "WebExtensions/What_does_review_rejection_mean_to_users", + "publish/what-does-review-rejection-mean-to-users/", + ), + ("AMO/Policy/Featured", "publish/recommended-extensions/"), + ) ) -)) +) diff --git a/tests/headless/test_cdn.py b/tests/headless/test_cdn.py index f3e5ecfd417..7a25063ab91 100644 --- a/tests/headless/test_cdn.py +++ b/tests/headless/test_cdn.py @@ -5,18 +5,20 @@ def is_cloudfront_cache_hit(response): """CloudFront specific check for evidence of a cache hit.""" - return (response.headers['x-cache'] in ('Hit from cloudfront', - 'RefreshHit from cloudfront')) + return response.headers["x-cache"] in ( + "Hit from cloudfront", + "RefreshHit from cloudfront", + ) def is_cloudfront_cache_miss(response): """CloudFront specific check for evidence of a cache miss.""" - return response.headers['x-cache'] == 'Miss from cloudfront' + return response.headers["x-cache"] == "Miss from cloudfront" def is_cloudfront_error(response): """CloudFront specific check for evidence of an error response.""" - return response.headers['x-cache'] == 'Error from cloudfront' + return response.headers["x-cache"] == "Error from cloudfront" def is_cdn_cache_hit(response): @@ -34,8 +36,9 @@ def is_cdn_error(response): return is_cloudfront_error(response) -def assert_not_cached_by_cdn(url, expected_status_code=200, method='get', - **request_kwargs): +def assert_not_cached_by_cdn( + url, expected_status_code=200, method="get", **request_kwargs +): response = request(method, url, **request_kwargs) assert response.status_code == expected_status_code if expected_status_code >= 400: @@ -45,29 +48,32 @@ def assert_not_cached_by_cdn(url, expected_status_code=200, method='get', return response -def assert_not_cached(url, expected_status_code=200, is_behind_cdn=True, - method='get', **request_kwargs): +def assert_not_cached( + url, expected_status_code=200, is_behind_cdn=True, method="get", **request_kwargs +): if is_behind_cdn: - response1 = assert_not_cached_by_cdn(url, expected_status_code, method, - **request_kwargs) - response2 = assert_not_cached_by_cdn(url, expected_status_code, method, - **request_kwargs) + response1 = assert_not_cached_by_cdn( + url, expected_status_code, method, **request_kwargs + ) + response2 = assert_not_cached_by_cdn( + url, expected_status_code, method, **request_kwargs + ) if expected_status_code in (301, 302): - assert (response2.headers['location'] == - response1.headers['location']) + assert response2.headers["location"] == response1.headers["location"] return response2 response = request(method, url, **request_kwargs) assert response.status_code == expected_status_code - assert 'no-cache' in response.headers['Cache-Control'] - assert 'no-store' in response.headers['Cache-Control'] - assert 'must-revalidate' in response.headers['Cache-Control'] - assert 'max-age=0' in response.headers['Cache-Control'] + assert "no-cache" in response.headers["Cache-Control"] + assert "no-store" in response.headers["Cache-Control"] + assert "must-revalidate" in response.headers["Cache-Control"] + assert "max-age=0" in response.headers["Cache-Control"] return response -def assert_cached(url, expected_status_code=200, is_behind_cdn=True, - method='get', **request_kwargs): +def assert_cached( + url, expected_status_code=200, is_behind_cdn=True, method="get", **request_kwargs +): response = request(method, url, **request_kwargs) assert response.status_code == expected_status_code if is_behind_cdn: @@ -78,49 +84,53 @@ def assert_cached(url, expected_status_code=200, is_behind_cdn=True, if expected_status_code == 200: assert response2.content == response.content elif expected_status_code in (301, 302): - assert (response2.headers['location'] == - response.headers['location']) + assert response2.headers["location"] == response.headers["location"] else: assert is_cdn_cache_hit(response) else: - assert 'public' in response.headers['Cache-Control'] - assert (('max-age' in response.headers['Cache-Control']) or - ('s-maxage' in response.headers['Cache-Control'])) + assert "public" in response.headers["Cache-Control"] + assert ("max-age" in response.headers["Cache-Control"]) or ( + "s-maxage" in response.headers["Cache-Control"] + ) return response @pytest.mark.headless @pytest.mark.nondestructive @pytest.mark.parametrize( - 'slug,status', [('/miel', 500), - ('/_kuma_status.json', 200), - ('/healthz', 204), - ('/readiness', 204), - ('/api/v1/whoami', 200), - ('/api/v1/search/en-US?q=css', 200), - ('/en-US/search?q=css', 200), - ('/en-US/profile', 302), - ('/en-US/profile/edit', 302), - ('/en-US/profiles/sheppy', 200), - ('/en-US/profiles/sheppy/edit', 403), - ('/en-US/profiles/sheppy/delete', 302), - ('/en-US/users/signin', 200), - ('/en-US/users/signup', 200), - ('/en-US/users/signout', 302), - ('/en-US/users/account/inactive', 200), - ('/en-US/users/account/signup', 302), - ('/en-US/users/account/signin/error', 200), - ('/en-US/users/account/signin/cancelled', 200), - ('/en-US/users/account/email', 302), - ('/en-US/users/account/email/confirm', 200), - ('/en-US/users/account/email/confirm/1', 200), - ('/en-US/users/account/recover/sent', 200), - ('/en-US/users/account/recover/done', 302), - ('/admin/login/', 200), - ('/admin/users/user/1/', 302), - ('/admin/wiki/document/purge/', 302), - ('/media/revision.txt', 200), - ('/media/kumascript-revision.txt', 200)]) + "slug,status", + [ + ("/miel", 500), + ("/_kuma_status.json", 200), + ("/healthz", 204), + ("/readiness", 204), + ("/api/v1/whoami", 200), + ("/api/v1/search/en-US?q=css", 200), + ("/en-US/search?q=css", 200), + ("/en-US/profile", 302), + ("/en-US/profile/edit", 302), + ("/en-US/profiles/sheppy", 200), + ("/en-US/profiles/sheppy/edit", 403), + ("/en-US/profiles/sheppy/delete", 302), + ("/en-US/users/signin", 200), + ("/en-US/users/signup", 200), + ("/en-US/users/signout", 302), + ("/en-US/users/account/inactive", 200), + ("/en-US/users/account/signup", 302), + ("/en-US/users/account/signin/error", 200), + ("/en-US/users/account/signin/cancelled", 200), + ("/en-US/users/account/email", 302), + ("/en-US/users/account/email/confirm", 200), + ("/en-US/users/account/email/confirm/1", 200), + ("/en-US/users/account/recover/sent", 200), + ("/en-US/users/account/recover/done", 302), + ("/admin/login/", 200), + ("/admin/users/user/1/", 302), + ("/admin/wiki/document/purge/", 302), + ("/media/revision.txt", 200), + ("/media/kumascript-revision.txt", 200), + ], +) def test_not_cached(site_url, is_behind_cdn, slug, status): """Ensure that these endpoints respond as expected and are not cached.""" assert_not_cached(site_url + slug, status, is_behind_cdn) @@ -129,50 +139,54 @@ def test_not_cached(site_url, is_behind_cdn, slug, status): @pytest.mark.headless @pytest.mark.nondestructive @pytest.mark.parametrize( - 'slug,status', [('/en-US/', 200), - ('/en-US/events', 302), - ('/robots.txt', 200), - ('/favicon.ico', 302), - ('/contribute.json', 200), - ('/humans.txt', 200), - ('/sitemap.xml', 200), - ('/sitemaps/en-US/sitemap.xml', 200), - ('/files/2767/hut.jpg', 301), - ('/@api/deki/files/3613/=hut.jpg', 301), - ('/diagrams/workflow/workflow.svg', 200), - ('/presentations/microsummaries/index.html', 200), - ('/api/v1/doc/en-US/Web/CSS', 200), - ('/en-US/search/xml', 200), - ('/en-US/docs.json?slug=Web/HTML', 200), - ('/en-US/Firefox', 302), - ('/en-US/docs/Web/HTML', 200), - ('/en-US/docs/Web/HTML$json', 200), - ('/en-US/docs/Web/HTML$children', 200)]) + "slug,status", + [ + ("/en-US/", 200), + ("/en-US/events", 302), + ("/robots.txt", 200), + ("/favicon.ico", 302), + ("/contribute.json", 200), + ("/humans.txt", 200), + ("/sitemap.xml", 200), + ("/sitemaps/en-US/sitemap.xml", 200), + ("/files/2767/hut.jpg", 301), + ("/@api/deki/files/3613/=hut.jpg", 301), + ("/diagrams/workflow/workflow.svg", 200), + ("/presentations/microsummaries/index.html", 200), + ("/api/v1/doc/en-US/Web/CSS", 200), + ("/en-US/search/xml", 200), + ("/en-US/docs.json?slug=Web/HTML", 200), + ("/en-US/Firefox", 302), + ("/en-US/docs/Web/HTML", 200), + ("/en-US/docs/Web/HTML$json", 200), + ("/en-US/docs/Web/HTML$children", 200), + ], +) def test_cached(site_url, is_behind_cdn, is_local_url, slug, status): """Ensure that these requests are cached.""" if is_local_url: - if any(slug.startswith(p) for p in - ('/diagrams/', '/presentations/', '/files/', '/@api/')): - pytest.xfail('attachments and legacy files are typically not ' - 'served from a local development instance') + if any( + slug.startswith(p) + for p in ("/diagrams/", "/presentations/", "/files/", "/@api/") + ): + pytest.xfail( + "attachments and legacy files are typically not " + "served from a local development instance" + ) assert_cached(site_url + slug, status, is_behind_cdn) @pytest.mark.headless @pytest.mark.nondestructive -@pytest.mark.parametrize( - 'zone', ['Add-ons', 'Apps', 'Firefox', 'Learn', 'Marketplace']) -@pytest.mark.parametrize( - 'slug', ['/{}', - '/{}$json', - '/{}$children']) +@pytest.mark.parametrize("zone", ["Add-ons", "Apps", "Firefox", "Learn", "Marketplace"]) +@pytest.mark.parametrize("slug", ["/{}", "/{}$json", "/{}$children"]) def test_no_locale_cached_302(site_url, is_behind_cdn, slug, zone): """ Ensure that these zone requests without a locale that should return 302 are cached. """ response = assert_cached(site_url + slug.format(zone), 302, is_behind_cdn) - assert response.headers['location'].startswith('/docs/') + assert response.headers["location"].startswith("/docs/") @pytest.mark.headless @@ -182,12 +196,10 @@ def test_document_with_cookie_and_param(site_url, is_behind_cdn, is_local_url): Ensure that the "django_language" cookie, and query parameters are forwarded/cached-on for document requests. """ - url = site_url + '/docs/Web/HTML' - response = assert_cached(url, 302, is_behind_cdn, - cookies={'django_language': 'de'}) - assert response.headers['location'].endswith('/de/docs/Web/HTML') - response = assert_cached(url, 302, is_behind_cdn, - cookies={'django_language': 'fr'}) - assert response.headers['location'].endswith('/fr/docs/Web/HTML') - response = assert_cached(url + '?lang=es', 302, is_behind_cdn) - assert response.headers['location'].endswith('/es/docs/Web/HTML') + url = site_url + "/docs/Web/HTML" + response = assert_cached(url, 302, is_behind_cdn, cookies={"django_language": "de"}) + assert response.headers["location"].endswith("/de/docs/Web/HTML") + response = assert_cached(url, 302, is_behind_cdn, cookies={"django_language": "fr"}) + assert response.headers["location"].endswith("/fr/docs/Web/HTML") + response = assert_cached(url + "?lang=es", 302, is_behind_cdn) + assert response.headers["location"].endswith("/es/docs/Web/HTML") diff --git a/tests/headless/test_endpoints.py b/tests/headless/test_endpoints.py index 1877f877fd5..7febef262ab 100644 --- a/tests/headless/test_endpoints.py +++ b/tests/headless/test_endpoints.py @@ -7,12 +7,14 @@ from . import INDEXED_WEB_DOMAINS, request -META_ROBOTS_RE = re.compile(r'''(?x) # Verbose regex mode +META_ROBOTS_RE = re.compile( + r"""(?x) # Verbose regex mode # end meta tag -''') +""" +) @pytest.fixture() @@ -24,125 +26,128 @@ def is_indexed(site_url): @pytest.mark.headless @pytest.mark.nondestructive @pytest.mark.parametrize( - 'slug', - ['/en-US/promote', - '/en-US/promote/buttons', - '/en-US/maintenance-mode', - '/en-US/unsubscribe/1', - '/en-US/dashboards', - '/en-US/dashboards/spam', - '/en-US/dashboards/revisions', - '/en-US/dashboards/macros', - '/en-US/dashboards/user_lookup?user=sheppy', - '/en-US/dashboards/topic_lookup?topic=mathml', - '/en-US/users/account/keys', - '/en-US/users/account/keys/new', - '/en-US/users/account/keys/1/history', - '/en-US/users/account/keys/1/delete', - '/en-US/users/ban/trump', - '/en-US/users/ban_user_and_cleanup/trump', - '/en-US/users/ban_user_and_cleanup_summary/trump', - '/en-US/contribute/', - '/en-US/docs/ckeditor_config.js', - '/en-US/docs/preview-wiki-content', - '/en-US/docs/all', - '/en-US/docs/new?slug=test', - '/en-US/docs/tags', - '/en-US/docs/tag/ARIA', - '/en-US/docs/needs-review', - '/en-US/docs/needs-review/editorial', - '/en-US/docs/localization-tag', - '/en-US/docs/localization-tag/inprogress', - '/en-US/docs/top-level', - '/en-US/docs/with-errors', - '/en-US/docs/without-parent', - '/en-US/docs/templates', - '/en-US/docs/submit_akismet_spam', - '/en-US/docs/Web/HTML?raw', - '/en-US/docs/Web/HTML$api', - '/en-US/docs/Web/HTML$toc', - '/en-US/docs/Web/HTML$edit', - '/en-US/docs/Web/HTML$move', - '/en-US/docs/Web/HTML$files', - '/en-US/docs/Web/HTML$purge', - '/en-US/docs/Web/HTML$delete', - '/en-US/docs/Web/HTML$history', - '/en-US/docs/Web/HTML$restore', - '/en-US/docs/Web/HTML$locales', - '/en-US/docs/Web/HTML$translate', - '/en-US/docs/Web/HTML$subscribe', - '/en-US/docs/Web/HTML$subscribe_to_tree', - '/en-US/docs/Web/HTML$quick-review', - '/en-US/docs/Web/HTML$revert/1293895', - '/en-US/docs/Web/HTML$revision/1293895', - '/en-US/docs/Web/HTML$repair_breadcrumbs', - '/en-US/docs/Web/HTML$compare?locale=en-US&to=1299417&from=1293895']) + "slug", + [ + "/en-US/promote", + "/en-US/promote/buttons", + "/en-US/maintenance-mode", + "/en-US/unsubscribe/1", + "/en-US/dashboards", + "/en-US/dashboards/spam", + "/en-US/dashboards/revisions", + "/en-US/dashboards/macros", + "/en-US/dashboards/user_lookup?user=sheppy", + "/en-US/dashboards/topic_lookup?topic=mathml", + "/en-US/users/account/keys", + "/en-US/users/account/keys/new", + "/en-US/users/account/keys/1/history", + "/en-US/users/account/keys/1/delete", + "/en-US/users/ban/trump", + "/en-US/users/ban_user_and_cleanup/trump", + "/en-US/users/ban_user_and_cleanup_summary/trump", + "/en-US/contribute/", + "/en-US/docs/ckeditor_config.js", + "/en-US/docs/preview-wiki-content", + "/en-US/docs/all", + "/en-US/docs/new?slug=test", + "/en-US/docs/tags", + "/en-US/docs/tag/ARIA", + "/en-US/docs/needs-review", + "/en-US/docs/needs-review/editorial", + "/en-US/docs/localization-tag", + "/en-US/docs/localization-tag/inprogress", + "/en-US/docs/top-level", + "/en-US/docs/with-errors", + "/en-US/docs/without-parent", + "/en-US/docs/templates", + "/en-US/docs/submit_akismet_spam", + "/en-US/docs/Web/HTML?raw", + "/en-US/docs/Web/HTML$api", + "/en-US/docs/Web/HTML$toc", + "/en-US/docs/Web/HTML$edit", + "/en-US/docs/Web/HTML$move", + "/en-US/docs/Web/HTML$files", + "/en-US/docs/Web/HTML$purge", + "/en-US/docs/Web/HTML$delete", + "/en-US/docs/Web/HTML$history", + "/en-US/docs/Web/HTML$restore", + "/en-US/docs/Web/HTML$locales", + "/en-US/docs/Web/HTML$translate", + "/en-US/docs/Web/HTML$subscribe", + "/en-US/docs/Web/HTML$subscribe_to_tree", + "/en-US/docs/Web/HTML$quick-review", + "/en-US/docs/Web/HTML$revert/1293895", + "/en-US/docs/Web/HTML$revision/1293895", + "/en-US/docs/Web/HTML$repair_breadcrumbs", + "/en-US/docs/Web/HTML$compare?locale=en-US&to=1299417&from=1293895", + ], +) def test_redirect_to_wiki(site_url, wiki_site_url, slug): """Ensure that these endpoints redirect to the wiki domain.""" - resp = request('get', site_url + slug) + resp = request("get", site_url + slug) assert resp.status_code == 301 - assert resp.headers['location'] == wiki_site_url + slug + assert resp.headers["location"] == wiki_site_url + slug @pytest.mark.headless @pytest.mark.nondestructive def test_redirect_localization_dashboard(site_url, wiki_site_url): for base_url in (site_url, wiki_site_url): - url = base_url + '/en-US/dashboards/localization' - resp = request('get', url) + url = base_url + "/en-US/dashboards/localization" + resp = request("get", url) assert resp.status_code == 301, url - assert resp.headers['location'] == '/docs/MDN/Doc_status/Overview', url + assert resp.headers["location"] == "/docs/MDN/Doc_status/Overview", url @pytest.mark.headless @pytest.mark.nondestructive def test_document_json(site_url): - url = site_url + '/en-US/docs/Web$json' - resp = request('get', url) + url = site_url + "/en-US/docs/Web$json" + resp = request("get", url) assert resp.status_code == 200 - assert resp.headers['Content-Type'] == 'application/json' - assert resp.headers['Access-Control-Allow-Origin'] == '*' + assert resp.headers["Content-Type"] == "application/json" + assert resp.headers["Access-Control-Allow-Origin"] == "*" @pytest.mark.headless @pytest.mark.nondestructive def test_document(site_url, is_indexed): - url = site_url + '/en-US/docs/Web' - resp = request('get', url) + url = site_url + "/en-US/docs/Web" + resp = request("get", url) assert resp.status_code == 200 - assert resp.headers['Content-Type'] == 'text/html; charset=utf-8' + assert resp.headers["Content-Type"] == "text/html; charset=utf-8" meta = META_ROBOTS_RE.search(resp.text) assert meta - content = meta.group('content') + content = meta.group("content") if is_indexed: - assert content == 'index, follow' + assert content == "index, follow" else: - assert content == 'noindex, nofollow' + assert content == "noindex, nofollow" @pytest.mark.headless @pytest.mark.nondestructive def test_user_document(site_url): - url = site_url + '/en-US/docs/User:anonymous:uitest' - resp = request('get', url) + url = site_url + "/en-US/docs/User:anonymous:uitest" + resp = request("get", url) assert resp.status_code == 200 - assert resp.headers['Content-Type'] == 'text/html; charset=utf-8' + assert resp.headers["Content-Type"] == "text/html; charset=utf-8" meta = META_ROBOTS_RE.search(resp.text) assert meta - content = meta.group('content') + content = meta.group("content") # Pages with legacy MindTouch namespaces like 'User:' never get # indexed, regardless of what the base url is - assert content == 'noindex, nofollow' + assert content == "noindex, nofollow" @pytest.mark.headless @pytest.mark.nondestructive def test_document_based_redirection(site_url): """Ensure that content-based redirects properly redirect.""" - url = site_url + '/en-US/docs/MDN/Promote' - resp = request('get', url) + url = site_url + "/en-US/docs/MDN/Promote" + resp = request("get", url) assert resp.status_code == 301 - assert resp.headers['Location'] == '/en-US/docs/MDN/About/Promote' + assert resp.headers["Location"] == "/en-US/docs/MDN/About/Promote" @pytest.mark.headless @@ -152,11 +157,11 @@ def test_document_based_redirection_suppression(site_url): Ensure that the redirect directive and not the content of the target page is displayed when content-based redirects are suppressed. """ - url = site_url + '/en-US/docs/MDN/Promote?redirect=no' - resp = request('get', url) + url = site_url + "/en-US/docs/MDN/Promote?redirect=no" + resp = request("get", url) assert resp.status_code == 200 - body = PyQuery(resp.text)('#wikiArticle') - assert body.text().startswith('REDIRECT ') + body = PyQuery(resp.text)("#wikiArticle") + assert body.text().startswith("REDIRECT ") assert body.find('a[href="/en-US/docs/MDN/About/Promote"]') @@ -164,53 +169,81 @@ def test_document_based_redirection_suppression(site_url): @pytest.mark.headless @pytest.mark.nondestructive def test_home(site_url, is_indexed): - url = site_url + '/en-US/' - resp = request('get', url) + url = site_url + "/en-US/" + resp = request("get", url) assert resp.status_code == 200 - assert resp.headers['Content-Type'] == 'text/html; charset=utf-8' + assert resp.headers["Content-Type"] == "text/html; charset=utf-8" meta = META_ROBOTS_RE.search(resp.text) assert meta - content = meta.group('content') + content = meta.group("content") if is_indexed: - assert content == 'index, follow' + assert content == "index, follow" else: - assert content == 'noindex, nofollow' + assert content == "noindex, nofollow" @pytest.mark.headless @pytest.mark.nondestructive def test_hreflang_basic(site_url): """Ensure that we're specifying the correct value for lang and hreflang.""" - url = site_url + '/en-US/docs/Web/HTTP' - resp = request('get', url) + url = site_url + "/en-US/docs/Web/HTTP" + resp = request("get", url) assert resp.status_code == 200 html = PyQuery(resp.text) - assert html.attr('lang') == 'en' + assert html.attr("lang") == "en" assert html.find('head > link[hreflang="en"][href="{}"]'.format(url)) @pytest.mark.headless @pytest.mark.nondestructive @pytest.mark.parametrize( - 'uri,expected_keys', - [('/api/v1/whoami', ('username', 'is_staff', 'is_authenticated', 'timezone', - 'is_beta_tester', 'avatar_url', 'is_superuser')), - ('/api/v1/doc/en-US/Web/CSS', (('documentData', ('locale', 'title', 'slug', - 'tocHTML', 'bodyHTML', - 'id', 'quickLinksHTML', - 'parents', 'translations', - 'wikiURL', 'summary', - 'language', - 'lastModified', - 'absoluteURL')), - 'redirectURL'))], - ids=('whoami', 'doc') + "uri,expected_keys", + [ + ( + "/api/v1/whoami", + ( + "username", + "is_staff", + "is_authenticated", + "timezone", + "is_beta_tester", + "avatar_url", + "is_superuser", + ), + ), + ( + "/api/v1/doc/en-US/Web/CSS", + ( + ( + "documentData", + ( + "locale", + "title", + "slug", + "tocHTML", + "bodyHTML", + "id", + "quickLinksHTML", + "parents", + "translations", + "wikiURL", + "summary", + "language", + "lastModified", + "absoluteURL", + ), + ), + "redirectURL", + ), + ), + ], + ids=("whoami", "doc"), ) def test_api_basic(site_url, uri, expected_keys): """Basic test of site's api endpoints.""" - resp = request('get', site_url + uri) + resp = request("get", site_url + uri) assert resp.status_code == 200 - assert resp.headers.get('content-type') == 'application/json' + assert resp.headers.get("content-type") == "application/json" data = resp.json() for item in expected_keys: if isinstance(item, tuple): @@ -226,8 +259,8 @@ def test_api_basic(site_url, uri, expected_keys): @pytest.mark.nondestructive def test_api_doc_404(site_url): """Ensure that the beta site's doc api returns 404 for unknown docs.""" - url = site_url + '/api/v1/doc/en-US/NoSuchPage' - resp = request('get', url) + url = site_url + "/api/v1/doc/en-US/NoSuchPage" + resp = request("get", url) assert resp.status_code == 404 @@ -237,70 +270,75 @@ def test_api_doc_404(site_url): # - django-language cookie settings (False to omit) # - ?lang param value (False to omit) LOCALE_SELECTORS = { - 'en-US': ('en-US', 'en-US', False, False), - 'es': ('es', 'es', False, False), - 'fr-cookie': ('fr', 'es', 'fr', False), - 'de-param': ('de', 'es', 'fr', 'de'), + "en-US": ("en-US", "en-US", False, False), + "es": ("es", "es", False, False), + "fr-cookie": ("fr", "es", "fr", False), + "de-param": ("de", "es", "fr", "de"), } @pytest.mark.headless @pytest.mark.nondestructive -@pytest.mark.parametrize('expected,accept,cookie,param', - LOCALE_SELECTORS.values(), - ids=list(LOCALE_SELECTORS)) @pytest.mark.parametrize( - 'slug', ['/search', - '/events', - '/profile', - '/profiles/sheppy', - '/users/signin', - '/unsubscribe/1', - '/promote', - '/docs.json?slug=Web/HTML', - '/docs/feeds/rss/l10n-updates', - '/docs/feeds/atom/files', - '/docs/feeds/rss/all', - '/docs/feeds/rss/needs-review', - '/docs/feeds/rss/needs-review/technical', - '/docs/feeds/rss/revisions', - '/docs/feeds/rss/tag/CSS' - '/docs/localization-tag/inprogress', - '/docs/all', - '/docs/new?slug=test', - '/docs/preview-wiki-content', - '/docs/ckeditor_config.js', - '/docs/needs-review/editorial', - '/docs/tag/ARIA', - '/docs/tags', - '/docs/top-level', - '/docs/with-errors', - '/docs/without-parent', - '/dashboards/spam', - '/dashboards/macros', - '/dashboards/revisions', - '/dashboards/localization', - '/dashboards/topic_lookup', - '/dashboards/user_lookup', - '/docs/Web/HTML', - '/docs/Web/HTML$json', - '/docs/Web/HTML$children', - '/docs/Web/HTML$edit', - '/docs/Web/HTML$move', - '/docs/Web/HTML$files', - '/docs/Web/HTML$purge', - '/docs/Web/HTML$delete', - '/docs/Web/HTML$history', - '/docs/Web/HTML$translate', - '/docs/Web/HTML$quick-review', - '/docs/Web/HTML$subscribe', - '/docs/Web/HTML$subscribe_to_tree', - '/docs/Web/HTML$revision/1293895', - '/docs/Web/HTML$repair_breadcrumbs', - '/docs/Learn/CSS/Styling_text/Fundamentals$toc', - '/docs/Learn/CSS/Styling_text/Fundamentals#Color', - '/docs/Web/HTML$compare?locale=en-US&to=1299417&from=1293895', - '/docs/Web/HTML$revert/1293895']) + "expected,accept,cookie,param", + LOCALE_SELECTORS.values(), + ids=list(LOCALE_SELECTORS), +) +@pytest.mark.parametrize( + "slug", + [ + "/search", + "/events", + "/profile", + "/profiles/sheppy", + "/users/signin", + "/unsubscribe/1", + "/promote", + "/docs.json?slug=Web/HTML", + "/docs/feeds/rss/l10n-updates", + "/docs/feeds/atom/files", + "/docs/feeds/rss/all", + "/docs/feeds/rss/needs-review", + "/docs/feeds/rss/needs-review/technical", + "/docs/feeds/rss/revisions", + "/docs/feeds/rss/tag/CSS" "/docs/localization-tag/inprogress", + "/docs/all", + "/docs/new?slug=test", + "/docs/preview-wiki-content", + "/docs/ckeditor_config.js", + "/docs/needs-review/editorial", + "/docs/tag/ARIA", + "/docs/tags", + "/docs/top-level", + "/docs/with-errors", + "/docs/without-parent", + "/dashboards/spam", + "/dashboards/macros", + "/dashboards/revisions", + "/dashboards/localization", + "/dashboards/topic_lookup", + "/dashboards/user_lookup", + "/docs/Web/HTML", + "/docs/Web/HTML$json", + "/docs/Web/HTML$children", + "/docs/Web/HTML$edit", + "/docs/Web/HTML$move", + "/docs/Web/HTML$files", + "/docs/Web/HTML$purge", + "/docs/Web/HTML$delete", + "/docs/Web/HTML$history", + "/docs/Web/HTML$translate", + "/docs/Web/HTML$quick-review", + "/docs/Web/HTML$subscribe", + "/docs/Web/HTML$subscribe_to_tree", + "/docs/Web/HTML$revision/1293895", + "/docs/Web/HTML$repair_breadcrumbs", + "/docs/Learn/CSS/Styling_text/Fundamentals$toc", + "/docs/Learn/CSS/Styling_text/Fundamentals#Color", + "/docs/Web/HTML$compare?locale=en-US&to=1299417&from=1293895", + "/docs/Web/HTML$revert/1293895", + ], +) def test_locale_selection(site_url, slug, expected, accept, cookie, param): """ Ensure that locale selection, which depends on the "lang" query @@ -312,56 +350,59 @@ def test_locale_selection(site_url, slug, expected, accept, cookie, param): assert accept, "accept must be set to the Accept-Langauge header value." request_kwargs = { - 'headers': { - 'X-Requested-With': 'XMLHttpRequest', - 'Accept-Language': accept - } + "headers": {"X-Requested-With": "XMLHttpRequest", "Accept-Language": accept} } if cookie: - request_kwargs['cookies'] = {'django_language': cookie} + request_kwargs["cookies"] = {"django_language": cookie} if param: - request_kwargs['params'] = {'lang': param} + request_kwargs["params"] = {"lang": param} - response = request('get', url, **request_kwargs) + response = request("get", url, **request_kwargs) assert response.status_code == 302 - assert response.headers['location'].startswith('/{}/'.format(expected)) + assert response.headers["location"].startswith("/{}/".format(expected)) @pytest.mark.headless @pytest.mark.nondestructive -@pytest.mark.parametrize('locale', [None, '/de']) -@pytest.mark.parametrize( - 'zone', ['Add-ons', 'Apps', 'Firefox', 'Learn', 'Marketplace']) +@pytest.mark.parametrize("locale", [None, "/de"]) +@pytest.mark.parametrize("zone", ["Add-ons", "Apps", "Firefox", "Learn", "Marketplace"]) @pytest.mark.parametrize( - 'slug', ['{}/{}$edit', - '{}/{}$move', - '{}/{}$files', - '{}/{}$purge', - '{}/{}$delete', - '{}/{}$translate', - '{}/{}$quick-review', - '{}/{}$revert/1284393', - '{}/{}$subscribe', - '{}/{}$subscribe_to_tree']) + "slug", + [ + "{}/{}$edit", + "{}/{}$move", + "{}/{}$files", + "{}/{}$purge", + "{}/{}$delete", + "{}/{}$translate", + "{}/{}$quick-review", + "{}/{}$revert/1284393", + "{}/{}$subscribe", + "{}/{}$subscribe_to_tree", + ], +) def test_former_vanity_302(wiki_site_url, slug, zone, locale): """Ensure that these former zone vanity URL's return 302.""" - locale = locale or '' + locale = locale or "" url = wiki_site_url + slug.format(locale, zone) - response = request('get', url) + response = request("get", url) assert response.status_code == 302 - assert response.headers['location'].startswith('{}/docs/'.format(locale)) - assert response.headers['location'].endswith(slug.format('', zone)) + assert response.headers["location"].startswith("{}/docs/".format(locale)) + assert response.headers["location"].endswith(slug.format("", zone)) @pytest.mark.headless @pytest.mark.nondestructive @pytest.mark.parametrize( - 'slug', ['/en-US/dashboards/user_lookup?user=sheppy', - '/en-US/dashboards/topic_lookup?topic=mathml']) + "slug", + [ + "/en-US/dashboards/user_lookup?user=sheppy", + "/en-US/dashboards/topic_lookup?topic=mathml", + ], +) def test_lookup_dashboards(wiki_site_url, slug): """Ensure that the topic and user dashboards require login.""" - response = request('get', wiki_site_url + slug) + response = request("get", wiki_site_url + slug) assert response.status_code == 302 - assert response.headers['location'].endswith( - '/users/signin?next=' + quote(slug)) + assert response.headers["location"].endswith("/users/signin?next=" + quote(slug)) diff --git a/tests/headless/test_redirects.py b/tests/headless/test_redirects.py index aaf740ac86b..b12d55b9c39 100644 --- a/tests/headless/test_redirects.py +++ b/tests/headless/test_redirects.py @@ -1,12 +1,17 @@ - - import pytest from utils.urls import assert_valid_url -from .map_301 import (GITHUB_IO_URLS, LEGACY_URLS, MARIONETTE_URLS, - MOZILLADEMOS_URLS, REDIRECT_URLS, SCL3_REDIRECT_URLS, - WEBEXT_URLS, ZONE_REDIRECT_URLS) +from .map_301 import ( + GITHUB_IO_URLS, + LEGACY_URLS, + MARIONETTE_URLS, + MOZILLADEMOS_URLS, + REDIRECT_URLS, + SCL3_REDIRECT_URLS, + WEBEXT_URLS, + ZONE_REDIRECT_URLS, +) # while these test methods are similar, they're each testing a # subset of redirects, and it was easier to work with them separately. @@ -14,71 +19,75 @@ @pytest.mark.headless @pytest.mark.nondestructive -@pytest.mark.parametrize('url', REDIRECT_URLS, - ids=[item['url'] for item in REDIRECT_URLS]) +@pytest.mark.parametrize( + "url", REDIRECT_URLS, ids=[item["url"] for item in REDIRECT_URLS] +) def test_redirects(url, base_url): - url['base_url'] = base_url + url["base_url"] = base_url assert_valid_url(**url) @pytest.mark.headless @pytest.mark.nondestructive -@pytest.mark.parametrize('url', GITHUB_IO_URLS, - ids=[item['url'] for item in GITHUB_IO_URLS]) +@pytest.mark.parametrize( + "url", GITHUB_IO_URLS, ids=[item["url"] for item in GITHUB_IO_URLS] +) def test_github_redirects(url, base_url): - url['base_url'] = base_url + url["base_url"] = base_url assert_valid_url(**url) @pytest.mark.headless @pytest.mark.nondestructive -@pytest.mark.parametrize('url', MOZILLADEMOS_URLS, - ids=[item['url'] for item in MOZILLADEMOS_URLS]) +@pytest.mark.parametrize( + "url", MOZILLADEMOS_URLS, ids=[item["url"] for item in MOZILLADEMOS_URLS] +) def test_mozillademos_redirects(url, base_url): - url['base_url'] = base_url + url["base_url"] = base_url assert_valid_url(**url) @pytest.mark.headless @pytest.mark.nondestructive -@pytest.mark.parametrize('url', LEGACY_URLS, - ids=[item['url'] for item in LEGACY_URLS]) +@pytest.mark.parametrize("url", LEGACY_URLS, ids=[item["url"] for item in LEGACY_URLS]) def test_legacy_urls(url, base_url): - url['base_url'] = base_url + url["base_url"] = base_url assert_valid_url(**url) @pytest.mark.headless @pytest.mark.nondestructive -@pytest.mark.parametrize('url', SCL3_REDIRECT_URLS, - ids=[item['url'] for item in SCL3_REDIRECT_URLS]) +@pytest.mark.parametrize( + "url", SCL3_REDIRECT_URLS, ids=[item["url"] for item in SCL3_REDIRECT_URLS] +) def test_slc3_redirects(url, base_url): - url['base_url'] = base_url + url["base_url"] = base_url assert_valid_url(**url) @pytest.mark.headless @pytest.mark.nondestructive -@pytest.mark.parametrize('url', ZONE_REDIRECT_URLS, - ids=[item['url'] for item in ZONE_REDIRECT_URLS]) +@pytest.mark.parametrize( + "url", ZONE_REDIRECT_URLS, ids=[item["url"] for item in ZONE_REDIRECT_URLS] +) def test_zone_redirects(url, base_url): - url['base_url'] = base_url + url["base_url"] = base_url assert_valid_url(**url) @pytest.mark.headless @pytest.mark.nondestructive -@pytest.mark.parametrize('url', MARIONETTE_URLS, - ids=[item['url'] for item in MARIONETTE_URLS]) +@pytest.mark.parametrize( + "url", MARIONETTE_URLS, ids=[item["url"] for item in MARIONETTE_URLS] +) def test_marionette_redirects(url, base_url): - url['base_url'] = base_url + url["base_url"] = base_url assert_valid_url(**url) @pytest.mark.headless @pytest.mark.nondestructive -@pytest.mark.parametrize('url', WEBEXT_URLS, - ids=[item['url'] for item in WEBEXT_URLS]) +@pytest.mark.parametrize("url", WEBEXT_URLS, ids=[item["url"] for item in WEBEXT_URLS]) def test_webext_redirects(url, base_url): - url['base_url'] = base_url + url["base_url"] = base_url assert_valid_url(**url) diff --git a/tests/headless/test_robots.py b/tests/headless/test_robots.py index 8e71636de8c..7c2ed714e08 100644 --- a/tests/headless/test_robots.py +++ b/tests/headless/test_robots.py @@ -10,16 +10,16 @@ @pytest.mark.headless @pytest.mark.nondestructive def test_robots(any_host_url): - url = any_host_url + '/robots.txt' + url = any_host_url + "/robots.txt" response = requests.get(url) assert response.status_code == 200 urlbits = urlsplit(any_host_url) hostname = urlbits.netloc if hostname in INDEXED_ATTACHMENT_DOMAINS: - assert response.text.strip() == '' + assert response.text.strip() == "" elif hostname in INDEXED_WEB_DOMAINS: - assert 'Sitemap: ' in response.text - assert 'Disallow: /admin/\n' in response.text + assert "Sitemap: " in response.text + assert "Disallow: /admin/\n" in response.text else: - assert 'Disallow: /\n' in response.text + assert "Disallow: /\n" in response.text diff --git a/tests/utils/urls.py b/tests/utils/urls.py index 214978b0ef9..466a56a6c9b 100644 --- a/tests/utils/urls.py +++ b/tests/utils/urls.py @@ -6,16 +6,24 @@ # https://github.com/mozilla/bedrock/blob/master/tests/redirects/base.py def get_abs_url(url, base_url): - if url.startswith('/'): + if url.startswith("/"): # urljoin messes with query strings too much - return ''.join([base_url, url]) + return "".join([base_url, url]) return url # https://github.com/mozilla/bedrock/blob/master/tests/redirects/base.py -def url_test(url, location=None, status_code=requests.codes.moved_permanently, - req_headers=None, req_kwargs=None, resp_headers=None, query=None, - follow_redirects=False, final_status_code=requests.codes.ok): +def url_test( + url, + location=None, + status_code=requests.codes.moved_permanently, + req_headers=None, + req_kwargs=None, + resp_headers=None, + query=None, + follow_redirects=False, + final_status_code=requests.codes.ok, +): r""" Function for producing a config dict for the redirect test. @@ -48,15 +56,15 @@ def url_test(url, location=None, status_code=requests.codes.moved_permanently, :return: dict or list of dicts """ test_data = { - 'url': url, - 'location': location, - 'status_code': status_code, - 'req_headers': req_headers, - 'req_kwargs': req_kwargs, - 'resp_headers': resp_headers, - 'query': query, - 'follow_redirects': follow_redirects, - 'final_status_code': final_status_code, + "url": url, + "location": location, + "status_code": status_code, + "req_headers": req_headers, + "req_kwargs": req_kwargs, + "resp_headers": resp_headers, + "query": query, + "follow_redirects": follow_redirects, + "final_status_code": final_status_code, } expanded_urls = list(braceexpand(url)) num_urls = len(expanded_urls) @@ -65,23 +73,31 @@ def url_test(url, location=None, status_code=requests.codes.moved_permanently, new_urls = [] if location: - expanded_locations = list(braceexpand(test_data['location'])) + expanded_locations = list(braceexpand(test_data["location"])) num_locations = len(expanded_locations) for i, url in enumerate(expanded_urls): data = test_data.copy() - data['url'] = url + data["url"] = url if location and num_urls == num_locations: - data['location'] = expanded_locations[i] + data["location"] = expanded_locations[i] new_urls.append(data) return new_urls -def assert_valid_url(url, location=None, status_code=requests.codes.moved_permanently, - req_headers=None, req_kwargs=None, resp_headers=None, - query=None, base_url=None, follow_redirects=False, - final_status_code=requests.codes.ok): +def assert_valid_url( + url, + location=None, + status_code=requests.codes.moved_permanently, + req_headers=None, + req_kwargs=None, + resp_headers=None, + query=None, + base_url=None, + follow_redirects=False, + final_status_code=requests.codes.ok, +): """ Define a test of a URL's response. :param url: The URL in question (absolute or relative). @@ -95,16 +111,16 @@ def assert_valid_url(url, location=None, status_code=requests.codes.moved_perman :param follow_redirects: Boolean indicating whether redirects should be followed. :param final_status_code: Expected status code after following any redirects. """ - kwargs = {'allow_redirects': follow_redirects} + kwargs = {"allow_redirects": follow_redirects} if req_headers: - kwargs['headers'] = req_headers + kwargs["headers"] = req_headers if req_kwargs: kwargs.update(req_kwargs) abs_url = get_abs_url(url, base_url) resp = requests.get(abs_url, **kwargs) # so that the value will appear in locals in test output - resp_location = resp.headers.get('location') + resp_location = resp.headers.get("location") if follow_redirects: assert resp.status_code == final_status_code else: @@ -120,14 +136,14 @@ def assert_valid_url(url, location=None, status_code=requests.codes.moved_perman resp_parsed = urlparse(resp_location) assert query == parse_qs(resp_parsed.query) # strip off query for further comparison - resp_location = resp_location.split('?')[0] + resp_location = resp_location.split("?")[0] assert location == unquote(resp_location) if resp_headers and not follow_redirects: def convert_to_set(header): - return frozenset(d.strip() for d in header.lower().split(',')) + return frozenset(d.strip() for d in header.lower().split(",")) for name, value in resp_headers.items(): assert name in resp.headers