├── kuma
├── api
│ ├── __init__.py
│ ├── v1
│ │ ├── __init__.py
│ │ ├── plus
│ │ │ ├── __init__.py
│ │ │ └── landing_page.py
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── test_subscriptions.py
│ │ │ ├── test_landing_page.py
│ │ │ ├── test_watched_items.py
│ │ │ ├── test_forms.py
│ │ │ ├── test_views.py
│ │ │ └── test_search.py
│ │ ├── forms.py
│ │ ├── urls.py
│ │ ├── api.py
│ │ ├── decorators.py
│ │ ├── pagination.py
│ │ ├── auth.py
│ │ ├── search
│ │ │ └── forms.py
│ │ └── views.py
│ ├── tests
│ │ ├── __init__.py
│ │ └── test_admin.py
│ ├── urls.py
│ ├── admin_urls.py
│ └── apps.py
├── core
│ ├── __init__.py
│ ├── exceptions.py
│ ├── tests
│ │ ├── logging_urls.py
│ │ ├── test_settings.py
│ │ ├── test_views.py
│ │ ├── test_validators.py
│ │ ├── __init__.py
│ │ ├── test_decorators.py
│ │ └── test_utils.py
│ ├── views.py
│ ├── admin.py
│ ├── apps.py
│ ├── tasks.py
│ ├── decorators.py
│ └── urlresolvers.py
├── health
│ ├── __init__.py
│ ├── tests
│ │ └── __init__.py
│ └── urls.py
├── users
│ ├── __init__.py
│ ├── migrations
│ │ ├── __init__.py
│ │ ├── 0004_remove_userprofile_fxa_uid.py
│ │ ├── 0005_userprofile_is_subscriber.py
│ │ ├── 0002_auto_20210922_1159.py
│ │ ├── 0003_auto_20211014_1214.py
│ │ ├── 0007_userprofile_subscription_type.py
│ │ ├── 0001_initial.py
│ │ └── 0006_accountevent.py
│ ├── apps.py
│ ├── utils.py
│ ├── tests
│ │ ├── test_auth.py
│ │ ├── test_views.py
│ │ ├── test_checks.py
│ │ └── test_tasks.py
│ ├── middleware.py
│ ├── models.py
│ ├── tasks.py
│ └── checks.py
├── bookmarks
│ ├── __init__.py
│ ├── migrations
│ │ ├── __init__.py
│ │ ├── 0002_remove_bookmark_notes.py
│ │ ├── 0003_auto_20220118_0409.py
│ │ └── 0001_initial.py
│ ├── apps.py
│ └── models.py
├── settings
│ ├── __init__.py
│ ├── local.py
│ ├── prod.py
│ └── pytest.py
├── version
│ ├── __init__.py
│ ├── tests
│ │ ├── __init__.py
│ │ └── test_views.py
│ ├── urls.py
│ └── views.py
├── documenturls
│ ├── __init__.py
│ ├── migrations
│ │ ├── __init__.py
│ │ └── 0001_initial.py
│ ├── apps.py
│ ├── admin.py
│ └── models.py
├── notifications
│ ├── __init__.py
│ ├── migrations
│ │ ├── __init__.py
│ │ ├── 0004_notificationdata_data.py
│ │ ├── 0010_notification_deleted.py
│ │ ├── 0003_watch_title.py
│ │ ├── 0009_notificationdata_page_url.py
│ │ ├── 0011_auto_20220210_0921.py
│ │ ├── 0007_watch_users.py
│ │ ├── 0002_auto_20211105_1529.py
│ │ ├── 0006_auto_20220105_0138.py
│ │ ├── 0005_auto_20211215_2004.py
│ │ ├── 0008_default_watch.py
│ │ └── 0001_initial.py
│ ├── apps.py
│ ├── admin.py
│ ├── management
│ │ └── commands
│ │ │ └── extract_notifications.py
│ ├── browsers.py
│ └── models.py
├── plus
│ ├── migrations
│ │ ├── __init__.py
│ │ ├── 0002_auto_20210723_1901.py
│ │ └── 0001_initial.py
│ ├── apps.py
│ ├── models.py
│ └── admin.py
├── attachments
│ ├── tests
│ │ ├── __init__.py
│ │ ├── conftest.py
│ │ └── test_views.py
│ ├── __init__.py
│ ├── apps.py
│ ├── urls.py
│ ├── utils.py
│ └── views.py
├── __init__.py
├── wsgi.py
├── celery.py
├── urls.py
└── conftest.py
├── Jenkinsfiles
├── prod-push.yml
├── stage-push.yml
├── standby-push.yml
├── default.groovy
├── main.groovy
└── push.groovy
├── tmp
└── emails
│ └── .keep
├── docs
├── images
│ └── rendering.png
├── elasticsearch.rst
├── index.rst
├── requirements.txt
├── docker.rst
├── documentation.rst
├── troubleshooting.rst
└── tests.rst
├── .gitmodules
├── .github
├── merge-when-green.yml
├── dependabot.yml
├── ISSUE_TEMPLATE
│ ├── support-request.md
│ ├── bug_report.md
│ ├── content-issue.md
│ ├── change-request.md
│ └── feature_request.md
└── workflows
│ ├── documentation-build.yml
│ ├── python-lints.yml
│ └── docker.yml
├── pytest.ini
├── docker
├── docker-mysql.repo
└── images
│ ├── kuma
│ ├── Dockerfile
│ └── README.rst
│ └── kuma_base
│ ├── README.rst
│ └── Dockerfile
├── scripts
├── ci-codecovsubmit
├── ci-localerefresh
├── start-worker
├── compile-mo.sh
├── ci-python
└── slack-notify.sh
├── .flake8
├── templates
└── admin
│ └── search
│ └── index
│ └── change_list.html
├── .editorconfig
├── jinja2
└── includes
│ └── translate_locales.html
├── SECURITY.md
├── renovate.json
├── manage.py
├── CODE_OF_CONDUCT.md
├── contribute.json
├── Jenkinsfile
├── .env-dist.dev
├── .gitignore
├── .dockerignore
├── pyproject.toml
├── Makefile
├── README.rst
└── docker-compose.yml
/kuma/api/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/kuma/api/v1/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/kuma/core/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/kuma/health/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/kuma/users/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/kuma/api/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/kuma/api/v1/plus/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/kuma/bookmarks/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/kuma/settings/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/kuma/version/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/kuma/api/v1/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/kuma/documenturls/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/kuma/health/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/kuma/notifications/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/kuma/plus/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/kuma/users/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/kuma/version/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/kuma/attachments/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/kuma/bookmarks/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/kuma/documenturls/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/kuma/notifications/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Jenkinsfiles/prod-push.yml:
--------------------------------------------------------------------------------
1 | pipeline:
2 | enabled: true
3 | script: "push"
4 |
--------------------------------------------------------------------------------
/Jenkinsfiles/stage-push.yml:
--------------------------------------------------------------------------------
1 | pipeline:
2 | enabled: true
3 | script: "push"
4 |
--------------------------------------------------------------------------------
/Jenkinsfiles/standby-push.yml:
--------------------------------------------------------------------------------
1 | pipeline:
2 | enabled: true
3 | script: "push"
4 |
--------------------------------------------------------------------------------
/tmp/emails/.keep:
--------------------------------------------------------------------------------
1 | This folder contains emails generated by the development environment
2 |
--------------------------------------------------------------------------------
/docs/images/rendering.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mdn/kuma/main/docs/images/rendering.png
--------------------------------------------------------------------------------
/kuma/attachments/__init__.py:
--------------------------------------------------------------------------------
1 | """kuma.attachments handles user-uploaded page attachments."""
2 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "locale"]
2 | path = locale
3 | url = https://github.com/mozilla-l10n/mdn-l10n.git
4 |
--------------------------------------------------------------------------------
/kuma/api/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import include, re_path
2 |
3 | urlpatterns = [
4 | re_path("^v1/", include("kuma.api.v1.urls")),
5 | ]
6 |
--------------------------------------------------------------------------------
/kuma/plus/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class PlusConfig(AppConfig):
5 | name = "kuma.plus"
6 | verbose_name = "Plus"
7 |
--------------------------------------------------------------------------------
/.github/merge-when-green.yml:
--------------------------------------------------------------------------------
1 | # From https://github.com/phstc/probot-merge-when-green#configuration
2 | mergeMethod: squash
3 | requireApprovalFromRequestedReviewers: true
4 |
--------------------------------------------------------------------------------
/kuma/bookmarks/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class BookmarksConfig(AppConfig):
5 | name = "kuma.bookmarks"
6 | verbose_name = "Bookmarks"
7 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | addopts = -rsxX --tb=native
3 | testpaths = kuma
4 | python_files = test*.py
5 | DJANGO_SETTINGS_MODULE = kuma.settings.pytest
6 | filterwarnings = default
7 |
--------------------------------------------------------------------------------
/docker/docker-mysql.repo:
--------------------------------------------------------------------------------
1 | [mysql-community-releases]
2 | name=Mysql Community Packages
3 | baseurl=http://repo.mysql.com/yum/mysql-community/el/6/$basearch/
4 | gpgcheck=0
5 | enabled=1
6 |
--------------------------------------------------------------------------------
/kuma/documenturls/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class DocumentURLsConfig(AppConfig):
5 | name = "kuma.documenturls"
6 | verbose_name = "Document URLs"
7 |
--------------------------------------------------------------------------------
/kuma/__init__.py:
--------------------------------------------------------------------------------
1 | # This will make sure the app is always imported when
2 | # Django starts so that shared_task will use this app.
3 | from .celery import app as celery_app
4 |
5 | __all__ = ("celery_app",)
6 |
--------------------------------------------------------------------------------
/kuma/notifications/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class NotificationsConfig(AppConfig):
5 | default_auto_field = "django.db.models.BigAutoField"
6 | name = "kuma.notifications"
7 |
--------------------------------------------------------------------------------
/kuma/version/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import re_path
2 |
3 | from . import views
4 |
5 | urlpatterns = [
6 | # Serve the revision hashes.
7 | re_path(r"^media/revision.txt$", views.revision_hash, name="version.kuma"),
8 | ]
9 |
--------------------------------------------------------------------------------
/kuma/api/admin_urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from kuma.api.v1.api import admin_api
4 | from kuma.api.v1.plus.notifications import admin_router
5 |
6 | admin_api.add_router("/", admin_router)
7 |
8 | urlpatterns = [
9 | path("", admin_api.urls),
10 | ]
11 |
--------------------------------------------------------------------------------
/scripts/ci-codecovsubmit:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e # Exit on non-zero status
3 | set -u # Treat unset variables as an error
4 |
5 | # FYI, as of April 2020, you get version 7.47.0 of curl
6 | # and `--retry-connrefused` was added in curl 7.52.0 :(
7 |
8 | bash <(curl -s --retry 3 https://codecov.io/bash)
9 |
--------------------------------------------------------------------------------
/docs/elasticsearch.rst:
--------------------------------------------------------------------------------
1 | ======
2 | Search
3 | ======
4 | Kuma uses Elasticsearch_ to power its on-site search.
5 |
6 | .. _Elasticsearch: https://www.elastic.co/elasticsearch/
7 |
8 | .. _indexing-documents:
9 |
10 | Indexing documents
11 | ==================
12 | Indexing is done outside Kuma. It's done by the Deployer in Yari.
13 |
--------------------------------------------------------------------------------
/kuma/health/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import re_path
2 |
3 | from . import views
4 |
5 | urlpatterns = [
6 | re_path(r"^healthz/?$", views.liveness, name="health.liveness"),
7 | re_path(r"^readiness/?$", views.readiness, name="health.readiness"),
8 | re_path(r"^_kuma_status.json$", views.status, name="health.status"),
9 | ]
10 |
--------------------------------------------------------------------------------
/kuma/core/exceptions.py:
--------------------------------------------------------------------------------
1 | class ProgrammingError(Exception):
2 | """Somebody made a mistake in the code."""
3 |
4 |
5 | class DateTimeFormatError(Exception):
6 | """Called by the datetimeformat function when receiving invalid format."""
7 |
8 | pass
9 |
10 |
11 | class FixtureMissingError(Exception):
12 | """Raise this if a fixture is missing"""
13 |
--------------------------------------------------------------------------------
/Jenkinsfiles/default.groovy:
--------------------------------------------------------------------------------
1 | stage('Build base') {
2 | utils.sh_with_notify(
3 | 'make build-base VERSION=latest',
4 | 'Build of latest-tagged Kuma base image'
5 | )
6 | }
7 |
8 | stage('Build & push images') {
9 | utils.sh_with_notify(
10 | 'make build-kuma push-kuma',
11 | "Build & push of commit-tagged Kuma image"
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/kuma/core/tests/logging_urls.py:
--------------------------------------------------------------------------------
1 | from django.core.exceptions import SuspiciousOperation
2 | from django.urls import re_path
3 |
4 | from kuma.core.urlresolvers import i18n_patterns
5 |
6 |
7 | def suspicious(request):
8 | raise SuspiciousOperation("Raising exception to test logging.")
9 |
10 |
11 | urlpatterns = i18n_patterns(re_path(r"^suspicious/$", suspicious, name="suspicious"))
12 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 |
2 | ====
3 | Kuma
4 | ====
5 |
6 | .. include:: ../README.rst
7 | :start-after: .. Omit badges from docs
8 |
9 | Contents:
10 |
11 | .. toctree::
12 | :maxdepth: 2
13 |
14 | installation
15 | development
16 | troubleshooting
17 | tests
18 | tests-functional
19 |
20 | celery
21 | elasticsearch
22 | documentation
23 |
24 | docker
25 | deploy
26 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | updates:
4 | - package-ecosystem: "github-actions"
5 | directory: "/"
6 | schedule:
7 | interval: "weekly"
8 |
9 | - package-ecosystem: "pip"
10 | directory: "/"
11 | schedule:
12 | interval: "weekly"
13 |
14 | - package-ecosystem: "docker"
15 | directory: "/docker/images/kuma_base"
16 | schedule:
17 | interval: "weekly"
18 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | # This is a standalone requirements file for the documentation requirements
2 | # used by ReadTheDocs to set up the documentation rendering environment
3 |
4 | # These should stay synced with other requirements files
5 | Babel==2.9.1
6 | docutils==0.17.1
7 | Jinja2==3.0.1
8 | pytz==2018.5
9 |
10 | # These requirements are unique to docs
11 | Pygments==2.10.0
12 | Sphinx==4.2.0
13 | snowballstemmer==2.1.0
14 |
--------------------------------------------------------------------------------
/kuma/attachments/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 | from django.utils.translation import gettext_lazy as _
3 |
4 |
5 | class AttachmentsConfig(AppConfig):
6 | """
7 | The Django App Config class to store information about the users app
8 | and do startup time things.
9 | """
10 |
11 | name = "kuma.attachments"
12 | verbose_name = _("Attachments")
13 |
14 | def ready(self):
15 | pass
16 |
--------------------------------------------------------------------------------
/kuma/bookmarks/migrations/0002_remove_bookmark_notes.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.10 on 2022-01-18 04:08
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("bookmarks", "0001_initial"),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name="bookmark",
15 | name="notes",
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | # Black recommends 88-char lines and ignoring the following lints:
3 | # - E203 - whitespace before ':'
4 | # - E501 - line too long
5 | # - W503 - line break before binary operator
6 | max-line-length=88
7 | ignore = E203, E501, W503
8 |
9 | # Allow star imports in config files:
10 | per-file-ignores =
11 | kuma/settings/local.py:F403,F405
12 | kuma/settings/prod.py:F403,F405
13 | kuma/settings/pytest.py:F403,F405
14 |
--------------------------------------------------------------------------------
/kuma/notifications/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from kuma.notifications.models import Notification, NotificationData, Watch
4 |
5 |
6 | @admin.register(Notification)
7 | class NotificationAdmin(admin.ModelAdmin):
8 | ...
9 |
10 |
11 | @admin.register(NotificationData)
12 | class NotificationDataAdmin(admin.ModelAdmin):
13 | ...
14 |
15 |
16 | @admin.register(Watch)
17 | class WatchAdmin(admin.ModelAdmin):
18 | ...
19 |
--------------------------------------------------------------------------------
/kuma/users/migrations/0004_remove_userprofile_fxa_uid.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.7 on 2021-10-20 07:25
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("users", "0003_auto_20211014_1214"),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name="userprofile",
15 | name="fxa_uid",
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/kuma/users/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 | from django.core.checks import register
3 | from django.utils.translation import gettext_lazy as _
4 |
5 |
6 | class UsersConfig(AppConfig):
7 | name = "kuma.users"
8 | verbose_name = _("Users")
9 |
10 | def ready(self):
11 | self.register_checks()
12 |
13 | def register_checks(self):
14 | from .checks import oidc_config_check
15 |
16 | register(oidc_config_check)
17 |
--------------------------------------------------------------------------------
/kuma/version/views.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.http import HttpResponse
3 | from django.views.decorators.cache import never_cache
4 | from django.views.decorators.http import require_safe
5 |
6 |
7 | @never_cache
8 | @require_safe
9 | def revision_hash(request):
10 | """
11 | Return the kuma revision hash.
12 | """
13 | return HttpResponse(
14 | settings.REVISION_HASH, content_type="text/plain; charset=utf-8"
15 | )
16 |
--------------------------------------------------------------------------------
/kuma/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for kuma project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "kuma.settings.local")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/templates/admin/search/index/change_list.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/change_list.html" %}
2 | {% load i18n %}
3 |
4 | {% block content_title %}
5 | {{ block.super }}
6 |
7 | {% if settings.ES_URLS %}
8 |
9 | {# 'safe_es_urls' comes from a context processor #}
10 |
{% blocktrans with backend=safe_es_urls|join:", " %}Elasticsearch server: {{ backend }}{% endblocktrans %}
11 |
12 | {% endif %}
13 | {% endblock %}
14 |
--------------------------------------------------------------------------------
/kuma/attachments/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import re_path
2 |
3 | from . import views
4 |
5 | urlpatterns = [
6 | re_path(
7 | r"^files/(?P\d+)/(?P.+)$",
8 | views.raw_file,
9 | name="attachments.raw_file",
10 | ),
11 | re_path(
12 | r"^@api/deki/files/(?P\d+)/=(?P.+)$",
13 | views.mindtouch_file_redirect,
14 | name="attachments.mindtouch_file_redirect",
15 | ),
16 | ]
17 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Top-most EditorConfig file ?
2 | root = true
3 |
4 | # Unix-style newlines with a newline ending every file
5 | [*]
6 | end_of_line = lf
7 | insert_final_newline = true
8 | trim_trailing_whitespace = true
9 |
10 | # Indentation
11 | indent_style = space
12 | indent_size = 4
13 |
14 | # YAML and Jenkins use two spaces
15 | [{*.yml,Jenkinsfile}]
16 | indent_size = 2
17 |
18 | # git and Makefile use the superior tabs
19 | [{.git*,Makefile,make.bat}]
20 | indent_style = tab
21 |
--------------------------------------------------------------------------------
/kuma/core/views.py:
--------------------------------------------------------------------------------
1 | from django.http import HttpResponse
2 |
3 | from kuma.core.decorators import shared_cache_control
4 |
5 |
6 | @shared_cache_control(s_maxage=60 * 60 * 24 * 30)
7 | def humans_txt(request):
8 | """We no longer maintain an actual /humans.txt endpoint but to avoid the
9 | sad 404 we instead now just encourage people to go and use the GitHub
10 | UI to see the contributors."""
11 | return HttpResponse("See https://github.com/mdn/kuma/graphs/contributors\n")
12 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/support-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Support request
3 | about: Request a support task from the MDN team.
4 | title: ''
5 | labels: 'status: needs triage'
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Summary**
11 | _What support task would you like completed by the team?_
12 |
13 |
14 | **Rationale**
15 | _What problems would this solve?_
16 |
17 |
18 | **Due Date**
19 | _When would you like this task completed by?_
20 |
21 |
22 | **Additional context**
23 | _Is there anything else we should know?_
24 |
--------------------------------------------------------------------------------
/kuma/notifications/migrations/0004_notificationdata_data.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.7 on 2021-12-13 15:05
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("notifications", "0003_watch_title"),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name="notificationdata",
15 | name="data",
16 | field=models.JSONField(default="{}"),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/jinja2/includes/translate_locales.html:
--------------------------------------------------------------------------------
1 | This template is automatically generated by *manage.py translate_locales_name* in order to make the languages name localizable. Do not edit it manually
2 | Background: https://bugzil.la/859499#c11
3 | {{ _('Chinese (Simplified)') }}
4 | {{ _('Chinese (Traditional)') }}
5 | {{ _('English (US)') }}
6 | {{ _('French') }}
7 | {{ _('German') }}
8 | {{ _('Japanese') }}
9 | {{ _('Korean') }}
10 | {{ _('Polish') }}
11 | {{ _('Portuguese (Brazilian)') }}
12 | {{ _('Russian') }}
13 | {{ _('Spanish') }}
14 |
--------------------------------------------------------------------------------
/kuma/users/migrations/0005_userprofile_is_subscriber.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.7 on 2021-12-15 12:31
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("users", "0004_remove_userprofile_fxa_uid"),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name="userprofile",
15 | name="is_subscriber",
16 | field=models.BooleanField(default=False),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/kuma/notifications/migrations/0010_notification_deleted.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.10 on 2022-01-27 14:36
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("notifications", "0009_notificationdata_page_url"),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name="notification",
15 | name="deleted",
16 | field=models.BooleanField(default=False),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/kuma/settings/local.py:
--------------------------------------------------------------------------------
1 | from .common import *
2 |
3 | # Settings for Docker Development
4 |
5 | INTERNAL_IPS = ("127.0.0.1",)
6 |
7 | # Default DEBUG to True, and recompute derived settings
8 | DEBUG = config("DEBUG", default=True, cast=bool)
9 | DEBUG_PROPAGATE_EXCEPTIONS = config(
10 | "DEBUG_PROPAGATE_EXCEPTIONS", default=DEBUG, cast=bool
11 | )
12 |
13 | PROTOCOL = config("PROTOCOL", default="https://")
14 | DOMAIN = config("DOMAIN", default="developer-local.allizom.org")
15 | SITE_URL = config("SITE_URL", default=PROTOCOL + DOMAIN)
16 |
--------------------------------------------------------------------------------
/kuma/notifications/management/commands/extract_notifications.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from django.core.management.base import BaseCommand
4 |
5 | from kuma.notifications.utils import process_changes
6 |
7 |
8 | class Command(BaseCommand):
9 | help = "Extracts notifications from a changes.json file"
10 |
11 | def add_arguments(self, parser):
12 | parser.add_argument("file", type=open)
13 |
14 | def handle(self, *args, **options):
15 | changes = json.loads(options["file"].read())
16 | process_changes(changes)
17 |
--------------------------------------------------------------------------------
/kuma/notifications/migrations/0003_watch_title.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.7 on 2021-11-06 13:06
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("notifications", "0002_auto_20211105_1529"),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name="watch",
15 | name="title",
16 | field=models.CharField(default="", max_length=2048),
17 | preserve_default=False,
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Reporting a Vulnerability
4 |
5 | To report a suspected security-related vulnerability within [MDN Web Docs](https://developer.mozilla.org), please use
6 | [this Bugzilla link](https://bugzilla.mozilla.org/enter_bug.cgi?product=developer.mozilla.org&component=Security), **and before submitting**, it is important to click the checkbox, just above the `Submit` button, entitled:
7 |
8 | ```
9 | This report is about a problem that is putting users at risk. It should be kept hidden from the public until it is resolved.
10 | ```
11 |
--------------------------------------------------------------------------------
/docker/images/kuma/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mdnwebdocs/kuma_base:latest
2 |
3 | ARG REVISION_HASH
4 | # Make the git commit hash permanently available within this image.
5 | ENV REVISION_HASH=$REVISION_HASH \
6 | DJANGO_SETTINGS_MODULE=kuma.settings.prod
7 |
8 | COPY --chown=kuma:kuma . /app
9 |
10 | # Temporarily enable candidate languages so assets are built for all
11 | # environments, but still defaults to disabled in production.
12 | # Also generate react.po translation files for beta.
13 | RUN ENABLE_CANDIDATE_LANGUAGES=True \
14 | make localecompile build-static
15 |
--------------------------------------------------------------------------------
/kuma/notifications/migrations/0009_notificationdata_page_url.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.7 on 2022-01-18 12:52
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("notifications", "0008_default_watch"),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name="notificationdata",
15 | name="page_url",
16 | field=models.TextField(default="/"),
17 | preserve_default=False,
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/scripts/ci-localerefresh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e # Exit on non-zero status
3 | set -u # Treat unset variables as an error
4 |
5 | # XXX Instead of using the "make-bundle" `localerefresh` we spell out
6 | # each and every one of these. Otherwise it causes to build-static again.
7 | ## docker-compose exec -T web make localerefresh
8 | docker-compose exec -T web make localeextract
9 | docker-compose exec -T web make localetest
10 | docker-compose exec -T web make localecompile
11 |
12 | cd locale
13 | export GIT_PAGER=cat
14 | git diff -G "^msgid " templates/LC_MESSAGES
15 |
--------------------------------------------------------------------------------
/Jenkinsfiles/main.groovy:
--------------------------------------------------------------------------------
1 | stage('Build base') {
2 | utils.sh_with_notify(
3 | 'make build-base VERSION=latest',
4 | 'Build of latest-tagged Kuma base image'
5 | )
6 | }
7 |
8 | stage('Test') {
9 | utils.compose_test()
10 | }
11 |
12 | stage('Build & push images') {
13 | utils.sh_with_notify(
14 | 'make build-kuma push-kuma',
15 | "Build & push of commit-tagged Kuma image"
16 | )
17 | utils.sh_with_notify(
18 | 'make push-base VERSION=latest',
19 | 'Push of latest-tagged Kuma base image'
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Help us fix things that are broken
4 | title: ''
5 | labels: 'status: needs triage'
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Summary**
11 | _What is the problem?_
12 |
13 |
14 | **Steps To Reproduce (STR)**
15 | _How can we reproduce the problem?_
16 |
17 | 1.
18 | 2.
19 | 3.
20 |
21 |
22 | **Actual behavior**
23 | _What actually happened?_
24 |
25 |
26 | **Expected behavior**
27 | _What did you expect to happen?_
28 |
29 |
30 | **Additional context**
31 | _Is there anything else we should know?_
32 |
--------------------------------------------------------------------------------
/kuma/api/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 | from django.conf import settings
3 | from elasticsearch_dsl.connections import connections
4 |
5 |
6 | class APIConfig(AppConfig):
7 | """
8 | The Django App Config class to store information about the API app
9 | and do startup time things.
10 | """
11 |
12 | name = "kuma.api"
13 | verbose_name = "API"
14 |
15 | def ready(self):
16 | # Configure Elasticsearch connections for connection pooling.
17 | connections.configure(
18 | default={"hosts": settings.ES_URLS},
19 | )
20 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/content-issue.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Content request
3 | about: Suggest a change to MDN Content
4 | title: ''
5 | labels: 'meta: content'
6 | assignees: 'chrisdavidmills'
7 |
8 | ---
9 |
10 |
11 | # Content Issue
12 |
13 | - [x] Please close this issue, I accidentally submitted it without adding any details
14 | - [ ] New documentation
15 | - [ ] Correction or update
16 |
17 | # URL of page you are seeing the problem on:
18 |
19 | # Details
20 |
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/change-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Change request
3 | about: Suggest a change to how MDN currently works
4 | title: ''
5 | labels: 'status: needs triage'
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Summary**
11 | _What should be changed? (Please provide a URL if applicable)_
12 |
13 |
14 | **Rationale**
15 | _What problems would this solve?_
16 |
17 |
18 | **Audience**
19 | _Who would use this changed feature?_
20 |
21 |
22 | **Proposal**
23 | _What would users see and do? What would happen as a result?_
24 |
25 |
26 | **Additional context**
27 | _Is there anything else we should know?_
28 |
--------------------------------------------------------------------------------
/kuma/notifications/migrations/0011_auto_20220210_0921.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.10 on 2022-02-10 09:21
2 |
3 | from django.db import migrations
4 |
5 |
6 | def delete_watchers(apps, schema_editor):
7 | Watch = apps.get_model("notifications", "Watch")
8 | Watch.objects.all().delete()
9 |
10 |
11 | class Migration(migrations.Migration):
12 |
13 | dependencies = [
14 | ("notifications", "0010_notification_deleted"),
15 | ]
16 |
17 | operations = [
18 | migrations.RunPython(
19 | code=delete_watchers, reverse_code=migrations.RunPython.noop
20 | )
21 | ]
22 |
--------------------------------------------------------------------------------
/scripts/start-worker:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e # Exit on non-zero status
3 | set -u # Treat unset variables as an error
4 |
5 | # This will make sure we don't even attempt to start the worker
6 | # until the $CELERY_BROKER_URL (i.e. Redis) is up and running.
7 | urlwait ${CELERY_BROKER_URL} 10
8 |
9 | # Note, in production you'll want to run a separate process with
10 | # just `celery -A kuma.celery:app beat ...`. But for docker-compose
11 | # it's fine to run it from the regular worker.
12 | exec celery -A kuma.celery:app worker -l info --beat --concurrency=${CELERY_WORKERS:-4} -Q mdn_purgeable,mdn_search,mdn_emails,mdn_wiki,mdn_api,celery
13 |
--------------------------------------------------------------------------------
/scripts/compile-mo.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # http://www.davidpashley.com/articles/writing-robust-shell-scripts/
3 | set -u # Exit with error on unset variables
4 | set -e # Exit with error if a command fails
5 |
6 | # syntax:
7 | # compile-mo.sh locale-dir/
8 |
9 | function usage() {
10 | echo "syntax:"
11 | echo "compile.sh locale-dir/"
12 | exit 1
13 | }
14 |
15 | # check if file and dir are there
16 | if [[ ($# -ne 1) || (! -d "$1") ]]; then usage; fi
17 |
18 | for lang in `find $1 -type f -name "*.po"`; do
19 | dir=`dirname $lang`
20 | stem=`basename $lang .po`
21 | msgfmt --check-header -o ${dir}/${stem}.mo $lang
22 | done
23 |
--------------------------------------------------------------------------------
/kuma/api/v1/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.conf import settings
3 |
4 |
5 | class AccountSettingsForm(forms.Form):
6 | locale = forms.ChoiceField(
7 | required=False,
8 | # The `settings.LANGUAGES` looks like this:
9 | # [('en-US', 'English (US)'), ...]
10 | # But the valid choices actually come from Yari which is the only
11 | # one that knows which ones are really valid.
12 | # Here in kuma we can't enforce it as string. But at least we
13 | # have a complete list of all possible languages
14 | choices=[(code, name) for code, name in settings.LANGUAGES],
15 | )
16 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base"
4 | ],
5 | "ignorePaths": [
6 | "package.json",
7 | "assets/ckeditor4/source/plugins/descriptionlist/package.json",
8 | "kuma/static/js/libs/jquery-ui-1.10.3.custom/development-bundle/package.json",
9 | "kumascript/package.json",
10 | "docker/images/kuma/Dockerfile"
11 | ],
12 | "masterIssue": true,
13 | "dockerfile": {
14 | "pinDigests": true
15 | },
16 | "docker-compose": {
17 | "enabled": false
18 | },
19 | "python": {
20 | "enabled": false
21 | },
22 | "rebaseStalePrs": true,
23 | "timezone": "UTC",
24 | "schedule": ["before 13:00 on mondays"]
25 | }
26 |
--------------------------------------------------------------------------------
/kuma/users/utils.py:
--------------------------------------------------------------------------------
1 | from kuma.users.models import UserProfile
2 |
3 |
4 | # Finds union of valid subscription id's from input and UserProfile.SubscriptionType.values
5 | # Should only be mdn_plus + one of 'SubscriptionType.values'
6 | def get_valid_subscription_type_or_none(input):
7 | subscription_types = list(set(input) & set(UserProfile.SubscriptionType.values))
8 | if len(subscription_types) > 1:
9 | print("Multiple subscriptions found in update %s" % subscription_types)
10 | # Sort array lexicographically. At least makes wrong answer consistent.
11 | subscription_types.sort()
12 |
13 | return subscription_types[0] if len(subscription_types) > 0 else ""
14 |
--------------------------------------------------------------------------------
/kuma/plus/migrations/0002_auto_20210723_1901.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.5 on 2021-07-23 19:01
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("plus", "0001_initial"),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name="landingpagesurvey",
15 | name="email",
16 | ),
17 | migrations.RemoveField(
18 | model_name="landingpagesurvey",
19 | name="user",
20 | ),
21 | migrations.RemoveField(
22 | model_name="landingpagesurvey",
23 | name="variant",
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/kuma/notifications/migrations/0007_watch_users.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.7 on 2022-01-05 19:37
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
11 | ("notifications", "0006_auto_20220105_0138"),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name="watch",
17 | name="users",
18 | field=models.ManyToManyField(
19 | through="notifications.UserWatch", to=settings.AUTH_USER_MODEL
20 | ),
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/kuma/bookmarks/migrations/0003_auto_20220118_0409.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.10 on 2022-01-18 04:09
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("bookmarks", "0002_remove_bookmark_notes"),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name="bookmark",
15 | name="custom_name",
16 | field=models.CharField(blank=True, max_length=500),
17 | ),
18 | migrations.AddField(
19 | model_name="bookmark",
20 | name="notes",
21 | field=models.CharField(blank=True, max_length=500),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest a new idea for MDN
4 | title: ''
5 | labels: 'status: needs triage'
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Summary**
11 | _What problem would this solve?_
12 |
13 |
14 | **Audience**
15 | _Who has this problem? Everyone? Article authors? Etc._
16 |
17 |
18 | **Rationale**
19 | _How do you know that people identified above actually have this problem?_
20 |
21 |
22 | **Workaround**
23 | _How are the people identified above currently handling this problem?_
24 |
25 |
26 | **Proposal**
27 | _Do you have suggestions for solving this problem?_
28 |
29 |
30 | **Additional context**
31 | _Is there anything else we should know?_
32 |
--------------------------------------------------------------------------------
/.github/workflows/documentation-build.yml:
--------------------------------------------------------------------------------
1 | name: Documentation Build
2 |
3 | on:
4 | pull_request:
5 | paths:
6 | - "docs/**"
7 | - .github/workflows/documentation-build.yml
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v3
15 |
16 | - uses: actions/setup-python@v3
17 | with:
18 | python-version: "3.8"
19 |
20 | - name: Install dependencies
21 | run: |
22 | python -m pip install --upgrade pip
23 | pip install --disable-pip-version-check -r docs/requirements.txt
24 |
25 | - name: Build docs
26 | run: |
27 | sphinx-build -b html -d doctrees -W docs html
28 |
--------------------------------------------------------------------------------
/docker/images/kuma/README.rst:
--------------------------------------------------------------------------------
1 | kuma
2 | ----
3 | The kuma Docker image builds on the kuma_base image, installing a kuma branch
4 | and building the assets needed for running as a webservice. The environment
5 | can be customized for different deployments.
6 |
7 | The image can be recreated locally with ``make build-kuma``.
8 |
9 | The image tagged ``latest`` is used by default for development. It can be
10 | created locally with ``make build-kuma VERSION=latest``. The official latest
11 | image is created from the main branch in Jenkins__ and published to
12 | DockerHub__.
13 |
14 | .. __: https://ci.us-west-2.mdn.mozit.cloud/blue/organizations/jenkins/kuma/branches/
15 | .. __: https://hub.docker.com/r/mdnwebdocs/kuma/
16 |
--------------------------------------------------------------------------------
/kuma/plus/models.py:
--------------------------------------------------------------------------------
1 | from uuid import uuid4
2 |
3 | from django.db import models
4 |
5 |
6 | class LandingPageSurvey(models.Model):
7 | uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
8 | # An insecure random string so that when a survey is submitted with more information
9 | # it can not easily be guessed and the ratelimit will make it impossible to try
10 | # all combinations.
11 | response = models.JSONField(editable=False, null=True)
12 | geo_information = models.TextField(editable=False, null=True)
13 | created = models.DateTimeField(auto_now_add=True)
14 | updated = models.DateTimeField(auto_now=True)
15 |
16 | def __str__(self):
17 | return str(self.uuid)
18 |
--------------------------------------------------------------------------------
/docker/images/kuma_base/README.rst:
--------------------------------------------------------------------------------
1 | kuma_base
2 | ---------
3 | The kuma_base Docker image contains the OS and libraries (C, Python, and
4 | Node.js) that support the kuma project. The kuma image extends this by
5 | installing the kuma source and building assets needed for production.
6 |
7 | The image can be recreated locally with ``make build-base``.
8 |
9 | The image tagged ``latest`` is used by default for development. It can be
10 | created locally with ``make build-base VERSION=latest``. The official
11 | latest image is created from the master branch in Jenkins__ and published to
12 | DockerHub__
13 |
14 | .. __: https://ci.us-west-2.mdn.mozit.cloud/blue/organizations/jenkins/kuma/branches/
15 | .. __: https://hub.docker.com/r/mdnwebdocs/kuma_base/
16 |
--------------------------------------------------------------------------------
/kuma/attachments/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import json
2 | from pathlib import Path
3 |
4 | import pytest
5 |
6 |
7 | @pytest.fixture
8 | def sample_attachment_redirect():
9 | redirects_file = Path(__file__).parent.parent / "redirects.json"
10 | with open(redirects_file) as f:
11 | redirects = json.load(f)
12 | first = list(redirects.keys())[0]
13 | return {"id": int(first), "url": redirects[first]}
14 |
15 |
16 | @pytest.fixture
17 | def sample_mindtouch_attachment_redirect():
18 | redirects_file = Path(__file__).parent.parent / "mindtouch_redirects.json"
19 | with open(redirects_file) as f:
20 | redirects = json.load(f)
21 | first = list(redirects.keys())[0]
22 | return {"id": int(first), "url": redirects[first]}
23 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 | import os
4 | import sys
5 |
6 |
7 | def main():
8 | """Run administrative tasks."""
9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "kuma.settings.local")
10 | try:
11 | from django.core.management import execute_from_command_line
12 | except ImportError as exc:
13 | raise ImportError(
14 | "Couldn't import Django. Are you sure it's installed and "
15 | "available on your PYTHONPATH environment variable? Did you "
16 | "forget to activate a virtual environment?"
17 | ) from exc
18 | execute_from_command_line(sys.argv)
19 |
20 |
21 | if __name__ == "__main__":
22 | main()
23 |
--------------------------------------------------------------------------------
/kuma/users/migrations/0002_auto_20210922_1159.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.7 on 2021-09-22 11:59
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("users", "0001_initial"),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name="userprofile",
15 | name="is_subscriber",
16 | ),
17 | migrations.RemoveField(
18 | model_name="userprofile",
19 | name="subscriber_number",
20 | ),
21 | migrations.AddField(
22 | model_name="userprofile",
23 | name="fxa_uid",
24 | field=models.CharField(blank=True, max_length=255, null=True, unique=True),
25 | ),
26 | ]
27 |
--------------------------------------------------------------------------------
/kuma/api/v1/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from . import search
4 | from .api import api
5 | from .plus.bookmarks import router as bookmarks_router
6 | from .plus.landing_page import router as landing_page_router
7 | from .plus.notifications import notifications_router, watch_router
8 | from .views import settings_router
9 |
10 | api.add_router("/settings", settings_router)
11 | api.add_router("/plus/notifications/", notifications_router)
12 | api.add_router("/plus/", watch_router)
13 | api.add_router("/plus/collection/", bookmarks_router)
14 | api.add_router("/plus/landing-page/", landing_page_router)
15 |
16 | urlpatterns = [
17 | path("", api.urls),
18 | path("search/", search.search, name="api.v1.search_legacy"),
19 | path("search", search.search, name="api.v1.search"),
20 | ]
21 |
--------------------------------------------------------------------------------
/kuma/users/tests/test_auth.py:
--------------------------------------------------------------------------------
1 | from django.test import RequestFactory
2 |
3 | from kuma.users.auth import logout_url
4 |
5 |
6 | # TODO: Check which new tests are needed.
7 | def test_logout_url(settings):
8 | request = RequestFactory().get("/some/path")
9 | request.session = {}
10 | url = logout_url(request)
11 | assert url == "/"
12 |
13 | request = RequestFactory().get("/some/path?next=/docs")
14 | request.session = {}
15 | url = logout_url(request)
16 | assert url == "/docs"
17 |
18 | settings.LOGOUT_REDIRECT_URL = "/loggedout"
19 | request = RequestFactory().get("/some/path")
20 | request.session = {}
21 | url = logout_url(request)
22 | assert url == "/loggedout"
23 |
24 | request.session["oidc_login_next"] = "/original"
25 | url = logout_url(request)
26 | assert url == "/original"
27 |
--------------------------------------------------------------------------------
/kuma/users/tests/test_views.py:
--------------------------------------------------------------------------------
1 | import urllib
2 |
3 | import pytest
4 | from django.urls import reverse
5 |
6 |
7 | @pytest.mark.django_db
8 | @pytest.mark.parametrize(
9 | "email", (None, "ringo@beatles.com"), ids=("without-email", "with-email")
10 | )
11 | def test_no_prompt_login(client, settings, email):
12 | params = {}
13 | if email:
14 | params.update(email=email)
15 | response = client.get(reverse("no_prompt_login"), data=params)
16 | assert response.status_code == 302
17 | location = response.headers.get("location")
18 | assert location
19 | location = urllib.parse.unquote(location)
20 | assert settings.OIDC_OP_AUTHORIZATION_ENDPOINT in location
21 | assert "next=/en-US/plus" in location
22 | if email:
23 | assert "prompt=none" in location
24 | assert f"login_hint={email}" in location
25 |
--------------------------------------------------------------------------------
/kuma/notifications/migrations/0002_auto_20211105_1529.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.7 on 2021-11-05 15:29
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("notifications", "0001_initial"),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name="watch",
15 | field=models.SlugField(blank=True),
16 | name="slug",
17 | preserve_default=False,
18 | ),
19 | migrations.RemoveField(
20 | model_name="watch",
21 | name="slug",
22 | ),
23 | migrations.AddField(
24 | model_name="watch",
25 | name="url",
26 | field=models.TextField(default=""),
27 | preserve_default=False,
28 | ),
29 | ]
30 |
--------------------------------------------------------------------------------
/kuma/users/migrations/0003_auto_20211014_1214.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.7 on 2021-10-14 12:14
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("users", "0002_auto_20210922_1159"),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name="userprofile",
15 | name="claims",
16 | ),
17 | migrations.AddField(
18 | model_name="userprofile",
19 | name="avatar",
20 | field=models.URLField(blank=True, default="", max_length=512),
21 | ),
22 | migrations.AddField(
23 | model_name="userprofile",
24 | name="fxa_refresh_token",
25 | field=models.CharField(blank=True, default="", max_length=128),
26 | ),
27 | ]
28 |
--------------------------------------------------------------------------------
/kuma/api/v1/api.py:
--------------------------------------------------------------------------------
1 | from django.http import HttpResponse
2 | from django.utils.cache import add_never_cache_headers
3 | from ninja import NinjaAPI
4 | from ratelimit.exceptions import Ratelimited
5 |
6 | from .auth import admin_auth, profile_auth
7 |
8 |
9 | class NoCacheNinjaAPI(NinjaAPI):
10 | def create_response(self, *args, **kwargs) -> HttpResponse:
11 | response = super().create_response(*args, **kwargs)
12 | add_never_cache_headers(response)
13 | return response
14 |
15 |
16 | api = NoCacheNinjaAPI(auth=profile_auth, csrf=True, version="v1")
17 |
18 | admin_api = NoCacheNinjaAPI(
19 | auth=admin_auth, csrf=False, version="v1", urls_namespace="admin_api"
20 | )
21 |
22 |
23 | @api.exception_handler(Ratelimited)
24 | def rate_limited(request, exc):
25 | return api.create_response(request, {"error": "Too many requests"}, status=429)
26 |
--------------------------------------------------------------------------------
/kuma/notifications/migrations/0006_auto_20220105_0138.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.7 on 2022-01-05 01:38
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("notifications", "0005_auto_20211215_2004"),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name="userwatch",
15 | name="browser_compatibility",
16 | field=models.JSONField(default=list),
17 | ),
18 | migrations.AddField(
19 | model_name="userwatch",
20 | name="content_updates",
21 | field=models.BooleanField(default=True),
22 | ),
23 | migrations.AddField(
24 | model_name="userwatch",
25 | name="custom",
26 | field=models.BooleanField(default=False),
27 | ),
28 | ]
29 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Mozilla Community Participation Guidelines
2 |
3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines.
4 | For more details, please read the
5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/).
6 |
7 | ## How to Report
8 |
9 | For more information on how to report violations of the CPG, please read our
10 | [How to Report](https://www.mozilla.org/en-US/about/governance/policies/participation/reporting/)
11 | page.
12 |
13 | ## Project Specific Etiquette
14 |
15 | See the [Contributing Guide][cg] for code standards, and the
16 | [Bugzilla Developer Etiquette Guidelines](bdeg) for general tips for being a
17 | helpful participant in an open source project.
18 |
19 | [cg]: https://github.com/mdn/kuma/blob/main/CONTRIBUTING.md
20 | [bdeg]: https://bugzilla.mozilla.org/page.cgi?id=etiquette.html
21 |
--------------------------------------------------------------------------------
/kuma/settings/prod.py:
--------------------------------------------------------------------------------
1 | from .common import *
2 |
3 | ATTACHMENT_HOST = config("ATTACHMENT_HOST", default="mdn.mozillademos.org")
4 | ALLOW_ROBOTS = config("ALLOW_ROBOTS", default=True, cast=bool)
5 |
6 | # Email
7 | DEFAULT_FROM_EMAIL = config(
8 | "DEFAULT_FROM_EMAIL", default="no-reply@developer.mozilla.org"
9 | )
10 | SERVER_EMAIL = config("SERVER_EMAIL", default="mdn-prod-noreply@mozilla.com")
11 |
12 | # Cache
13 | CACHES["default"]["TIMEOUT"] = 60 * 60 * 24
14 |
15 | MEDIA_URL = config("MEDIA_URL", default="https://developer.cdn.mozilla.net/media/")
16 |
17 | CELERY_TASK_ALWAYS_EAGER = config("CELERY_TASK_ALWAYS_EAGER", False, cast=bool)
18 | CELERYD_MAX_TASKS_PER_CHILD = (
19 | config("CELERYD_MAX_TASKS_PER_CHILD", default=500, cast=int) or None
20 | )
21 |
22 | ES_INDEX_PREFIX = config("ES_INDEX_PREFIX", default="mdnprod")
23 | ES_LIVE_INDEX = config("ES_LIVE_INDEX", default=True, cast=bool)
24 |
--------------------------------------------------------------------------------
/kuma/users/migrations/0007_userprofile_subscription_type.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.10 on 2022-03-03 13:45
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("users", "0006_accountevent"),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name="userprofile",
15 | name="subscription_type",
16 | field=models.CharField(
17 | blank=True,
18 | choices=[
19 | ("mdn_plus_5m", "MDN Plus 5M"),
20 | ("mdn_plus_5y", "MDN Plus 5Y"),
21 | ("mdn_plus_10m", "MDN Plus 10M"),
22 | ("mdn_plus_10y", "MDN Plus 10Y"),
23 | ("", "None"),
24 | ],
25 | default="",
26 | max_length=512,
27 | ),
28 | ),
29 | ]
30 |
--------------------------------------------------------------------------------
/kuma/version/tests/test_views.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from kuma.core.tests import assert_no_cache_header
4 | from kuma.core.urlresolvers import reverse
5 |
6 |
7 | @pytest.mark.parametrize("method", ["get", "head"])
8 | def test_revision_hash(client, db, method, settings):
9 | settings.REVISION_HASH = "the_revision_hash"
10 | response = getattr(client, method)(reverse("version.kuma"))
11 | assert response.status_code == 200
12 | assert response["Content-Type"] == "text/plain; charset=utf-8"
13 | assert_no_cache_header(response)
14 | if method == "get":
15 | assert response.content.decode() == "the_revision_hash"
16 |
17 |
18 | @pytest.mark.parametrize("method", ["post", "put", "delete", "options", "patch"])
19 | def test_revision_hash_405s(client, db, method):
20 | response = getattr(client, method)(reverse("version.kuma"))
21 | assert response.status_code == 405
22 | assert_no_cache_header(response)
23 |
--------------------------------------------------------------------------------
/docs/docker.rst:
--------------------------------------------------------------------------------
1 | ======
2 | Docker
3 | ======
4 |
5 | Docker__ is used for development and for deployment.
6 |
7 | .. __: https://www.docker.com
8 |
9 | Docker Images
10 | =============
11 | Docker images are used in development, usually with the local
12 | working files mounted in the images to set behaviour.
13 |
14 | Images are built by Jenkins__, after tests pass, and are
15 | published to DockerHub__. We try to
16 | `store the configuration in the environment`_, so that the
17 | published images can be used in deployments by setting
18 | environment variables to deployment-specific values.
19 |
20 | .. __: https://ci.us-west-2.mdn.mozit.cloud
21 | .. __: https://hub.docker.com/r/mdnwebdocs/kuma/
22 | .. _`store the configuration in the environment`: https://12factor.net/config
23 |
24 | Here are some of the images used in the Kuma project:
25 |
26 | .. Published images
27 | .. include:: ../docker/images/kuma/README.rst
28 | .. include:: ../docker/images/kuma_base/README.rst
29 |
--------------------------------------------------------------------------------
/kuma/core/tests/test_settings.py:
--------------------------------------------------------------------------------
1 | """Check that settings are consistent."""
2 |
3 |
4 | import pytest
5 | from django.conf import settings
6 |
7 |
8 | def test_accepted_locales():
9 | """Check for a consistent ACCEPTED_LOCALES."""
10 | assert len(settings.ACCEPTED_LOCALES) == len(set(settings.ACCEPTED_LOCALES))
11 | assert settings.ACCEPTED_LOCALES[0] == settings.LANGUAGE_CODE
12 |
13 |
14 | @pytest.mark.parametrize(
15 | "primary,secondary",
16 | (("zh-CN", "zh-TW"),),
17 | )
18 | def test_preferred_locale_codes(primary, secondary):
19 | assert settings.ACCEPTED_LOCALES.index(primary) < settings.ACCEPTED_LOCALES.index(
20 | secondary
21 | )
22 |
23 |
24 | @pytest.mark.parametrize("alias,locale", settings.LOCALE_ALIASES.items())
25 | def test_locale_aliases(alias, locale):
26 | """Check that each locale alias matches a supported locale."""
27 | assert alias not in settings.ACCEPTED_LOCALES
28 | assert alias == alias.lower()
29 | assert locale in settings.ACCEPTED_LOCALES
30 |
--------------------------------------------------------------------------------
/.github/workflows/python-lints.yml:
--------------------------------------------------------------------------------
1 | name: Python Lints
2 |
3 | on:
4 | pull_request:
5 | paths:
6 | - "kuma/**/*.py"
7 | - .github/workflows/python-lints.yml
8 | # This is in case Dependabot updates 'black'
9 | - pyproject.toml
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v3
17 |
18 | - uses: actions/setup-python@v3
19 | with:
20 | python-version: "3.8"
21 |
22 | - name: Install dependencies
23 | run: |
24 | python -m pip install --upgrade pip
25 | pip install --disable-pip-version-check black flake8 flake8-isort
26 | echo "Version of black installed:"
27 | black --version
28 | echo "Version of flake8 installed:"
29 | flake8 --version
30 |
31 | - name: Lint with flake8
32 | run: |
33 | flake8 kuma docs
34 |
35 | - name: Lint with black
36 | run: |
37 | black --check --diff kuma docs
38 |
--------------------------------------------------------------------------------
/contribute.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Kuma",
3 | "description": "The backend app powering MDN Web Docs.",
4 | "repository": {
5 | "url": "https://github.com/mdn/kuma",
6 | "license": "MPL-2.0",
7 | "tests": "https://github.com/mdn/kuma/actions"
8 | },
9 | "participate": {
10 | "home": "https://wiki.mozilla.org/MDN",
11 | "docs": "https://kuma.readthedocs.io/",
12 | "mailing-list": "https://discourse.mozilla.org/c/mdn",
13 | "chat": {
14 | "url": "https://mozilla.slack.com/archives/CLTBV3W9X"
15 | }
16 | },
17 | "bugs": {
18 | "list": "https://github.com/mdn/kuma/issues",
19 | "report": "https://github.com/mdn/kuma/issues/new/choose"
20 | },
21 | "urls": {
22 | "prod": "https://developer.mozilla.org/",
23 | "stage": "https://developer.allizom.org/"
24 | },
25 | "keywords": [
26 | "python",
27 | "django",
28 | "docker",
29 | "mysql",
30 | "elasticsearch"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/kuma/notifications/migrations/0005_auto_20211215_2004.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.7 on 2021-12-15 20:04
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("notifications", "0004_notificationdata_data"),
10 | ]
11 |
12 | operations = [
13 | migrations.DeleteModel(
14 | name="CompatibilityData",
15 | ),
16 | migrations.AddField(
17 | model_name="notification",
18 | name="starred",
19 | field=models.BooleanField(default=False),
20 | ),
21 | migrations.AddField(
22 | model_name="notificationdata",
23 | name="type",
24 | field=models.CharField(
25 | choices=[("content", "content"), ("compat", "compat")],
26 | default="compat",
27 | max_length=32,
28 | ),
29 | ),
30 | migrations.AlterField(
31 | model_name="notificationdata",
32 | name="data",
33 | field=models.JSONField(default=dict),
34 | ),
35 | ]
36 |
--------------------------------------------------------------------------------
/kuma/celery.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from celery import Celery
4 |
5 | # set the default Django settings module for the 'celery' program.
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "kuma.settings.local")
7 |
8 | app = Celery("kuma")
9 |
10 | # Using a string here means the worker doesn't have to serialize
11 | # the configuration object to child processes.
12 | # - namespace='CELERY' means all celery-related configuration keys
13 | # should have a `CELERY_` prefix.
14 | app.config_from_object("django.conf:settings", namespace="CELERY")
15 |
16 | # Load task modules from all registered Django app configs.
17 | app.autodiscover_tasks()
18 |
19 |
20 | @app.task(bind=True)
21 | def debug_task(self):
22 | print("Request: {0!r}".format(self.request))
23 |
24 |
25 | @app.task()
26 | def debug_task_returning(a, b):
27 | """Useful to see if the results backend is working.
28 | And it also checks that called with a `datetime.date`
29 | it gets that as parameters in the task."""
30 | import datetime
31 |
32 | assert isinstance(a, datetime.date), type(a)
33 | assert isinstance(b, datetime.date), type(b)
34 | return a < b
35 |
--------------------------------------------------------------------------------
/Jenkinsfiles/push.groovy:
--------------------------------------------------------------------------------
1 | stage("Announce") {
2 | utils.announce_push()
3 | }
4 |
5 | stage("Check Pull") {
6 | // Check that the image can be successfully pulled from the registry.
7 | utils.ensure_pull()
8 | }
9 |
10 | stage("Prepare Infra") {
11 | // Checkout the "mdn/infra" repo's "main" branch into the
12 | // "infra" sub-directory of the current working directory.
13 | utils.checkout_repo('https://github.com/mdn/infra', 'main', 'infra')
14 | }
15 |
16 | stage('Push') {
17 | dir('infra/apps/mdn/mdn-aws/k8s') {
18 | def current_revision_hash = utils.get_revision_hash()
19 | withEnv(["TO_REVISION_HASH=${env.GIT_COMMIT}",
20 | "FROM_REVISION_HASH=${current_revision_hash}"]) {
21 | // Run the database migrations.
22 | utils.migrate_db()
23 | // Start a rolling update of the Kuma-based deployments.
24 | utils.rollout()
25 | // Monitor the rollout until it has completed.
26 | utils.monitor_rollout()
27 | // Record the rollout in external services like New-Relic.
28 | utils.record_rollout()
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/kuma/notifications/browsers.py:
--------------------------------------------------------------------------------
1 | browsers = {
2 | "chrome": {
3 | "name": "Chrome",
4 | "preview_name": "Canary",
5 | },
6 | "chrome_android": {
7 | "name": "Chrome Android",
8 | },
9 | "deno": {
10 | "name": "Deno",
11 | },
12 | "edge": {
13 | "name": "Edge",
14 | },
15 | "firefox": {
16 | "name": "Firefox",
17 | "preview_name": "Nightly",
18 | },
19 | "firefox_android": {
20 | "name": "Firefox for Android",
21 | "pref_url": "about:config",
22 | },
23 | "ie": {
24 | "name": "Internet Explorer",
25 | },
26 | "nodejs": {
27 | "name": "Node.js",
28 | },
29 | "opera": {
30 | "name": "Opera",
31 | },
32 | "opera_android": {
33 | "name": "Opera Android",
34 | },
35 | "safari": {
36 | "name": "Safari",
37 | "preview_name": "TP",
38 | },
39 | "safari_ios": {
40 | "name": "Safari on iOS",
41 | },
42 | "samsunginternet_android": {
43 | "name": "Samsung Internet",
44 | },
45 | "webview_android": {
46 | "name": "WebView Android",
47 | },
48 | }
49 |
--------------------------------------------------------------------------------
/kuma/bookmarks/models.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import User
2 | from django.db import models
3 |
4 | from kuma.documenturls.models import DocumentURL
5 |
6 |
7 | class Bookmark(models.Model):
8 | documenturl = models.ForeignKey(
9 | DocumentURL, on_delete=models.CASCADE, verbose_name="Document URL"
10 | )
11 | user = models.ForeignKey(User, on_delete=models.CASCADE)
12 | custom_name = models.CharField(max_length=500, blank=True)
13 | notes = models.CharField(max_length=500, blank=True)
14 | deleted = models.DateTimeField(null=True)
15 | created = models.DateTimeField(auto_now_add=True)
16 | modified = models.DateTimeField(auto_now=True)
17 |
18 | class Meta:
19 | verbose_name = "Bookmark"
20 | unique_together = ["documenturl", "user_id"]
21 |
22 | def save(self, *args, **kwargs):
23 | if self.custom_name == self.documenturl.metadata["title"]:
24 | self.custom_name = ""
25 | super().save(*args, **kwargs)
26 |
27 | def __str__(self):
28 | return self.documenturl.uri
29 |
30 | @property
31 | def title(self):
32 | return self.custom_name or self.documenturl.metadata["title"]
33 |
--------------------------------------------------------------------------------
/docker/images/kuma_base/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.10.1-slim@sha256:9f702aa0f2bd7fe7a43bcf46e487040ba3237f2115994ae74ea7b270479ea8f3
2 |
3 | # Set the environment variables
4 | ENV PYTHONDONTWRITEBYTECODE=1 \
5 | PYTHONUNBUFFERED=1 \
6 | PIP_DISABLE_PIP_VERSION_CHECK=1 \
7 | # gunicorn concurrency
8 | WEB_CONCURRENCY=4
9 |
10 | RUN set -x \
11 | && apt-get update \
12 | && apt-get install -y --no-install-recommends \
13 | curl \
14 | dirmngr \
15 | # Needed for pytz to be able to install
16 | libsasl2-modules \
17 | gettext \
18 | build-essential \
19 | # Needed for Python to build cffi
20 | libffi-dev
21 |
22 | # add non-privileged user
23 | RUN useradd --uid 1000 --shell /bin/bash --create-home kuma \
24 | && mkdir -p app \
25 | && chown kuma:kuma /app \
26 | && chmod 775 /app
27 |
28 | # install Python libraries
29 | WORKDIR /app
30 | COPY --chown=kuma:kuma ./pyproject.toml ./poetry.lock /app/
31 | RUN pip install poetry~=1.1.12 \
32 | && POETRY_VIRTUALENVS_CREATE=false poetry install --no-root \
33 | && rm -rf ~/.cache/pip ~/.cache/pypoetry/cache
34 |
35 | # setup default run parameters
36 | USER kuma
37 | WORKDIR /app
38 | EXPOSE 8000
39 |
--------------------------------------------------------------------------------
/.github/workflows/docker.yml:
--------------------------------------------------------------------------------
1 | name: Docker testing
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | env:
15 | UID: 0
16 |
17 | steps:
18 | - uses: actions/checkout@v3
19 |
20 | - name: Environment info
21 | run: |
22 | docker-compose --version
23 |
24 | - name: Build docker
25 | run: docker-compose build
26 |
27 | - name: DB migrations
28 | run: |
29 | docker-compose run -T testing urlwait postgresql://kuma:kuma@postgres:5432/developer_mozilla_org 30
30 | docker-compose run -T testing ./manage.py migrate
31 | # Essentially compares **/models.py with **/migrations/*.py and
32 | # makes sure the developer didn't forget to create a new migration.
33 | docker-compose run -T testing ./manage.py makemigrations --check --dry-run
34 |
35 | - name: Run Python tests
36 | run: |
37 | docker-compose run -T testing make coveragetest
38 |
39 | - name: Submit code coverage
40 | run: |
41 | bash <(curl -s --retry 3 --retry-connrefused https://codecov.io/bash)
42 |
--------------------------------------------------------------------------------
/kuma/api/v1/decorators.py:
--------------------------------------------------------------------------------
1 | from functools import wraps
2 |
3 | from django.http import HttpResponseForbidden
4 |
5 | from .auth import NotASubscriber, is_subscriber
6 |
7 |
8 | def allow_CORS_GET(func):
9 | """Decorator to allow CORS for GET requests"""
10 |
11 | @wraps(func)
12 | def inner(request, *args, **kwargs):
13 | response = func(request, *args, **kwargs)
14 | if "GET" == request.method:
15 | response["Access-Control-Allow-Origin"] = "*"
16 | return response
17 |
18 | return inner
19 |
20 |
21 | def require_subscriber(view_function):
22 | """Check if a user is authorized to retrieve a resource.
23 |
24 | * Check if a user is logged in through kuma (django session).
25 | * Check if there is a bearer token in the request header.
26 | - Validate the token.
27 | - Create a new user if there is not one
28 | - Retrieve the resource for that user
29 | """
30 |
31 | @wraps(view_function)
32 | def is_authorized(request, *args, **kwargs):
33 | try:
34 | assert is_subscriber(request, raise_error=True)
35 | except NotASubscriber as e:
36 | return HttpResponseForbidden(e)
37 | return view_function(request, *args, **kwargs)
38 |
39 | return is_authorized
40 |
--------------------------------------------------------------------------------
/kuma/documenturls/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from .models import DocumentURL, DocumentURLCheck
4 |
5 |
6 | @admin.register(DocumentURL)
7 | class DocumentURLAdmin(admin.ModelAdmin):
8 | list_display = (
9 | "uri",
10 | "absolute_url",
11 | "is_valid",
12 | "modified",
13 | )
14 | readonly_fields = (
15 | "metadata",
16 | "invalid",
17 | "created",
18 | "modified",
19 | )
20 |
21 | list_filter = ("invalid",)
22 | search_fields = ("uri",)
23 | ordering = ("-created",)
24 | list_per_page = 10
25 |
26 | def is_valid(self, obj):
27 | return not obj.invalid
28 |
29 |
30 | @admin.register(DocumentURLCheck)
31 | class DocumentURLCheckAdmin(admin.ModelAdmin):
32 | list_display = (
33 | "_document_url",
34 | "http_error",
35 | "created",
36 | )
37 | readonly_fields = (
38 | "document_url",
39 | "http_error",
40 | "headers",
41 | "created",
42 | )
43 |
44 | list_filter = ("http_error",)
45 | search_fields = ("document_url__uri", "document_url__absolute_url")
46 | ordering = ("-created",)
47 | list_per_page = 10
48 |
49 | def _document_url(self, obj):
50 | return obj.document_url.absolute_url
51 |
--------------------------------------------------------------------------------
/kuma/core/admin.py:
--------------------------------------------------------------------------------
1 | # from django.contrib import admin
2 |
3 | # from rest_framework.authtoken.admin import TokenAdmin
4 |
5 | # from kuma.core.models import IPBan
6 |
7 |
8 | class DisabledDeleteActionMixin(object):
9 | def get_actions(self, request):
10 | """
11 | Remove the built-in delete action, since it bypasses the model
12 | delete() method (bad) and we want people using the non-admin
13 | deletion UI anyway.
14 | """
15 | actions = super(DisabledDeleteActionMixin, self).get_actions(request)
16 | if "delete_selected" in actions:
17 | del actions["delete_selected"]
18 | return actions
19 |
20 |
21 | class DisabledDeletionMixin(DisabledDeleteActionMixin):
22 | def has_delete_permission(self, request, obj=None):
23 | """
24 | Disable deletion of individual Documents, by always returning
25 | False for the permission check.
26 | """
27 | return False
28 |
29 |
30 | # @admin.register(IPBan)
31 | # class IPBanAdmin(admin.ModelAdmin):
32 | # # Remove list delete action to enforce model soft delete in admin site
33 | # actions = None
34 | # readonly_fields = ("deleted",)
35 | # list_display = ("ip", "created", "deleted")
36 |
37 |
38 | # TokenAdmin.raw_id_fields = ["user"]
39 |
--------------------------------------------------------------------------------
/kuma/api/v1/tests/test_subscriptions.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | import pytest
4 | from django.urls import reverse
5 |
6 | from kuma.users import models
7 |
8 |
9 | @pytest.mark.django_db
10 | def test_subscriptions(subscriber_client, wiki_user):
11 | response = subscriber_client.get(reverse("api-v1:whoami"))
12 | assert response.status_code == 200
13 | assert json.loads(response.content) == {
14 | "username": wiki_user.username,
15 | "is_authenticated": True,
16 | "email": wiki_user.email,
17 | "is_subscriber": True,
18 | "subscription_type": "",
19 | "avatar_url": "",
20 | }
21 |
22 | # Add Subscription and save model back to db
23 | profile = models.UserProfile.objects.get(user=wiki_user)
24 | profile.subscription_type = models.UserProfile.SubscriptionType.MDN_PLUS_10Y
25 | profile.is_subscriber = True
26 | profile.save()
27 |
28 | # Assert subscription type present in response
29 | response = subscriber_client.get(reverse("api-v1:whoami"))
30 | assert response.status_code == 200
31 | assert json.loads(response.content) == {
32 | "username": wiki_user.username,
33 | "is_authenticated": True,
34 | "email": wiki_user.email,
35 | "is_subscriber": True,
36 | "subscription_type": models.UserProfile.SubscriptionType.MDN_PLUS_10Y.value,
37 | "avatar_url": "",
38 | }
39 |
--------------------------------------------------------------------------------
/kuma/core/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 | from django.conf import settings
3 | from django.utils.functional import cached_property
4 | from django.utils.translation import gettext_lazy as _
5 |
6 | from kuma.celery import app
7 |
8 |
9 | class CoreConfig(AppConfig):
10 | """
11 | The Django App Config class to store information about the core app
12 | and do startup time things.
13 | """
14 |
15 | name = "kuma.core"
16 | verbose_name = _("Core")
17 |
18 | def ready(self):
19 | """Configure kuma.core after models are loaded."""
20 | self.add_periodc_tasks()
21 |
22 | def add_periodc_tasks(self):
23 | from kuma.core.tasks import clean_sessions, clear_old_notifications
24 |
25 | # Clean up expired sessions every 60 minutes
26 | app.add_periodic_task(60 * 60, clean_sessions.s())
27 | # Delete old notifications every month
28 | app.add_periodic_task(60 * 60 * 24 * 30, clear_old_notifications.s())
29 |
30 | @cached_property
31 | def language_mapping(self):
32 | """
33 | a static mapping of lower case language names and their native names
34 | """
35 | # LANGUAGES settings return a list of tuple with language code and their native name
36 | # Make the language code lower and convert the tuple to dictionary
37 | return {lang[0].lower(): lang[1] for lang in settings.LANGUAGES}
38 |
--------------------------------------------------------------------------------
/kuma/core/tests/test_views.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from django.core import mail
4 | from django.test import override_settings
5 | from django.utils.log import AdminEmailHandler
6 |
7 | from . import KumaTestCase
8 |
9 |
10 | @override_settings(
11 | DEBUG=False,
12 | DEBUG_PROPAGATE_EXCEPTIONS=False,
13 | ADMINS=(("admin", "admin@example.com"),),
14 | ROOT_URLCONF="kuma.core.tests.logging_urls",
15 | )
16 | class LoggingTests(KumaTestCase):
17 | logger = logging.getLogger("django.security")
18 | suspicous_path = "/en-US/suspicious/"
19 |
20 | def setUp(self):
21 | super(LoggingTests, self).setUp()
22 | self.old_handlers = self.logger.handlers[:]
23 |
24 | def tearDown(self):
25 | super(LoggingTests, self).tearDown()
26 | self.logger.handlers = self.old_handlers
27 |
28 | def test_no_mail_handler(self):
29 | self.logger.handlers = [logging.NullHandler()]
30 | response = self.client.get(self.suspicous_path)
31 | assert 400 == response.status_code
32 | assert 0 == len(mail.outbox)
33 |
34 | def test_mail_handler(self):
35 | self.logger.handlers = [AdminEmailHandler()]
36 | response = self.client.get(self.suspicous_path)
37 | assert 400 == response.status_code
38 | assert 1 == len(mail.outbox)
39 |
40 | assert "admin@example.com" in mail.outbox[0].to
41 | assert self.suspicous_path in mail.outbox[0].body
42 |
--------------------------------------------------------------------------------
/kuma/api/v1/plus/landing_page.py:
--------------------------------------------------------------------------------
1 | from uuid import UUID
2 |
3 | from django.middleware.csrf import get_token
4 | from django.shortcuts import get_object_or_404
5 | from ninja import Form, Router
6 | from pydantic import Json
7 | from ratelimit.decorators import ratelimit
8 |
9 | from kuma.api.v1.plus.notifications import Ok
10 | from kuma.plus.models import LandingPageSurvey
11 |
12 | router = Router()
13 |
14 |
15 | @router.post("/survey/", response=Ok, url_name="plus.landing_page.survey", auth=None)
16 | @ratelimit(group="landing_page_survey", key="user_or_ip", rate="100/m", block=True)
17 | def post_survey(request, uuid: str = Form(...), response: Json = Form(...)):
18 | survey = get_object_or_404(LandingPageSurvey, uuid=uuid)
19 | survey.response = response
20 | survey.save()
21 | return True
22 |
23 |
24 | @router.get("/survey/", url_name="plus.landing_page.survey", auth=None)
25 | @ratelimit(group="landing_page_survey", key="user_or_ip", rate="100/m", block=True)
26 | def get_survey(request, uuid: UUID = None):
27 | # Inspired by https://github.com/mdn/kuma/pull/7849/files
28 | if uuid:
29 | survey = get_object_or_404(LandingPageSurvey, uuid=uuid)
30 | else:
31 | geo_information = request.META.get("HTTP_CLOUDFRONT_VIEWER_COUNTRY_NAME") or ""
32 | survey = LandingPageSurvey.objects.create(
33 | geo_information=geo_information,
34 | )
35 | return {"uuid": survey.uuid, "csrfmiddlewaretoken": get_token(request)}
36 |
--------------------------------------------------------------------------------
/kuma/plus/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from .models import LandingPageSurvey
4 |
5 |
6 | class HasResponseFilter(admin.SimpleListFilter):
7 | title = "Has response"
8 | parameter_name = "has_response"
9 |
10 | def lookups(self, request, model_admin):
11 | return (
12 | ("true", "Has response"),
13 | ("false", "No response"),
14 | )
15 |
16 | def queryset(self, request, queryset):
17 | if self.value() == "true":
18 | return queryset.filter(response__isnull=False)
19 | if self.value() == "false":
20 | return queryset.filter(response__isnull=True)
21 |
22 |
23 | @admin.register(LandingPageSurvey)
24 | class LandingPageSurveyAdmin(admin.ModelAdmin):
25 | list_display = (
26 | "uuid",
27 | "geo_information",
28 | "has_response",
29 | "created",
30 | )
31 | fields = (
32 | "geo_information",
33 | "response",
34 | )
35 | readonly_fields = (
36 | "geo_information",
37 | "response",
38 | )
39 |
40 | list_filter = (HasResponseFilter,)
41 | search_fields = ("response", "uuid", "geo_information")
42 | ordering = ("-created",)
43 | list_per_page = 10
44 |
45 | def has_email(self, obj):
46 | return bool(obj.email)
47 |
48 | def has_response(self, obj):
49 | return bool(obj.response)
50 |
51 | def signed_in(self, obj):
52 | return bool(obj.user)
53 |
--------------------------------------------------------------------------------
/kuma/api/v1/tests/test_landing_page.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from kuma.core.urlresolvers import reverse
4 | from kuma.plus.models import LandingPageSurvey
5 |
6 |
7 | @pytest.mark.django_db
8 | def test_ping_landing_page_survey_bad_request(client):
9 | url = reverse("api-v1:plus.landing_page.survey")
10 |
11 | # Not a valid UUID
12 | response = client.get(url, {"uuid": "xxx"})
13 | assert response.status_code == 422
14 |
15 | # Not a recognized UUID
16 | response = client.get(url, {"uuid": "88f7a689-454a-4647-99bf-d62fa66da24a"})
17 | assert response.status_code == 404
18 |
19 | # No UUID in post
20 | response = client.post(url)
21 | assert response.status_code == 422
22 |
23 | response = client.get(url, {"variant": "1"})
24 | assert response.status_code == 200
25 | # Invalid JSON
26 | response = client.post(url, {"uuid": response.json()["uuid"], "response": "{{{{"})
27 | assert response.status_code == 422
28 |
29 |
30 | @pytest.mark.django_db
31 | def test_ping_landing_page_survey_reuse_uuid(client):
32 | url = reverse("api-v1:plus.landing_page.survey")
33 | response1 = client.get(url, HTTP_CLOUDFRONT_VIEWER_COUNTRY_NAME="Sweden")
34 | assert response1.status_code == 200
35 | assert LandingPageSurvey.objects.all().count() == 1
36 | response2 = client.get(
37 | url,
38 | {"uuid": response1.json()["uuid"]},
39 | HTTP_CLOUDFRONT_VIEWER_COUNTRY_NAME="USA",
40 | )
41 | assert response2.json()["uuid"] == response1.json()["uuid"]
42 | assert LandingPageSurvey.objects.all().count() == 1
43 |
--------------------------------------------------------------------------------
/kuma/api/tests/test_admin.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from django.conf import settings
4 | from django.urls import reverse
5 | from model_bakery import baker
6 |
7 | from kuma.notifications import models
8 |
9 |
10 | def test_admin_update_content(user_client, wiki_user):
11 | # Prepare: Watch page.
12 | page_title = "