├── donate ├── __init__.py ├── recaptcha │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_fields.py │ │ └── test_utils.py │ ├── fields.py │ └── utils.py ├── utility │ ├── __init__.py │ ├── faker │ │ ├── __init__.py │ │ └── helpers.py │ └── middleware.py ├── core │ ├── tests │ │ ├── __init__.py │ │ └── test_templatetags.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ ├── makemessages.py │ │ │ ├── enqueue_pontoon_sync.py │ │ │ ├── clear_fake_data.py │ │ │ └── load_fake_data.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0003_auto_20230531_2044.py │ │ └── 0002_featureflags.py │ ├── templatetags │ │ ├── __init__.py │ │ ├── auth_tags.py │ │ ├── analytics_tags.py │ │ ├── form_tags.py │ │ └── util_tags.py │ ├── factory │ │ └── __init__.py │ ├── constants.py │ ├── translation.py │ ├── feature_flags.py │ ├── utils.py │ ├── blocks.py │ ├── pontoon.py │ └── __init__.py ├── payments │ ├── tests │ │ ├── __init__.py │ │ ├── test_braintree_webhooks.py │ │ └── test_forms.py │ ├── migrations │ │ └── __init__.py │ ├── exceptions.py │ ├── wagtail_hooks.py │ ├── __init__.py │ ├── braintree_webhooks.py │ ├── urls.py │ ├── stripe_webhooks.py │ ├── constants.py │ └── sqs.py ├── users │ ├── migrations │ │ ├── __init__.py │ │ └── 0002_auto_20210602_1559.py │ ├── __init__.py │ ├── models.py │ ├── apps.py │ └── admin.py ├── templates │ ├── blocks │ │ ├── heading_block.html │ │ ├── image_block.html │ │ ├── menu_item.html │ │ └── accordion_block.html │ ├── fragments │ │ ├── donate_form_fundraise_up.html │ │ ├── messages.html │ │ ├── ga_events.html │ │ ├── menu_item.html │ │ ├── campaign_page_fundraiseup_form.html │ │ ├── campaign_page_intro.html │ │ ├── language_switcher.html │ │ ├── donate_form_disclaimer_fundraise_up.html │ │ └── donate_form_disclaimer_master.html │ ├── pages │ │ ├── base_page.html │ │ └── core │ │ │ ├── ways_to_give_page.html │ │ │ ├── contributor_support_page.html │ │ │ ├── content_page.html │ │ │ ├── landing_page.html │ │ │ └── campaign_page.html │ ├── payment │ │ ├── card.html │ │ ├── thank_you.html │ │ ├── card_upsell.html │ │ ├── paypal_upsell.html │ │ ├── newsletter_signup.html │ │ ├── includes │ │ │ └── trigger_ab_testing_thank_you_event.html │ │ └── newsletter_signup_master.html │ ├── forms │ │ ├── _field.html │ │ ├── _label.html │ │ └── form_field.html │ ├── wagtailadmin │ │ └── login.html │ ├── base.html │ ├── 500.html │ ├── 404.html │ ├── admin │ │ └── login.html │ ├── 403.html │ └── tags │ │ └── primarynav.html ├── settings │ ├── salesforce.py │ ├── database.py │ ├── sentry.py │ ├── braintree.py │ ├── __init__.py │ ├── s3.py │ ├── redis.py │ ├── testing.py │ ├── review_app.py │ ├── secure.py │ ├── staging.py │ ├── production.py │ ├── development.py │ ├── oidc.py │ └── languages.py ├── thunderbird │ └── templates │ │ ├── pages │ │ └── core │ │ │ ├── campaign_page.html │ │ │ ├── landing_page.html │ │ │ ├── contributor_support_page.html │ │ │ └── ways_to_give_page.html │ │ ├── fragments │ │ ├── donate_form.html │ │ └── donate_form_disclaimer.html │ │ ├── payment │ │ ├── paypal_upsell.html │ │ ├── newsletter_signup.html │ │ ├── card_upsell.html │ │ ├── card.html │ │ └── thank_you.html │ │ └── base.html ├── wsgi.py └── views.py ├── .prettierignore ├── .eslintignore ├── runtime.txt ├── invoke.yaml ├── source ├── sass │ ├── base │ │ ├── _utils.scss │ │ ├── _base.scss │ │ └── _typography.scss │ ├── components │ │ ├── _logo-item.scss │ │ ├── _rich-text.scss │ │ ├── _image-feature.scss │ │ ├── _introduction.scss │ │ ├── _accordion.scss │ │ ├── _support-form.scss │ │ ├── _card.scss │ │ ├── _app.scss │ │ ├── _error.scss │ │ ├── _icon.scss │ │ ├── _column-spacing.scss │ │ ├── _link.scss │ │ ├── _page-header.scss │ │ ├── _sticky-message.scss │ │ ├── _tabs.scss │ │ ├── _logo-showcase.scss │ │ ├── _nav-item.scss │ │ ├── _notification.scss │ │ ├── _heading.scss │ │ ├── _loading.scss │ │ ├── _accordion-item.scss │ │ ├── _payments.scss │ │ ├── _footer-links.scss │ │ ├── _primary-nav.scss │ │ ├── _burger.scss │ │ ├── _donation-update.scss │ │ ├── _form.scss │ │ ├── _footer-custom-select.scss │ │ ├── _newsletter-signup.scss │ │ ├── _donation-amount.scss │ │ ├── _header.scss │ │ ├── _form-custom-select.scss │ │ └── _hero.scss │ ├── abstracts │ │ └── _functions.scss │ ├── pages │ │ └── ways-to-give-page.scss │ └── main.scss ├── images │ ├── og.png │ ├── og-image_2x.png │ ├── favicon │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── mstile-150x150.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-256x256.png │ │ ├── browserconfig.xml │ │ ├── site.webmanifest │ │ └── safari-pinned-tab.svg │ ├── mozilla-logo-m.svg │ ├── mozilla-logo-black.svg │ └── mozilla-logo-white.svg └── js │ ├── components │ ├── analytics.js │ ├── recaptcha.js │ ├── env.js │ ├── donation-currency-width.js │ ├── waypoint-detection.js │ ├── copy-url.js │ ├── menu-toggle.js │ ├── tabs.js │ ├── accordion.js │ ├── validation.js │ └── post-code-validation.js │ └── payments-paypal-upsell.js ├── dockerfiles ├── Dockerfile.node └── Dockerfile.python ├── dev-requirements.in ├── tox.ini ├── CODEOWNERS ├── release-steps.sh ├── .stylelintrc ├── maintenance └── static │ └── _images │ ├── favicon │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── site.webmanifest │ └── safari-pinned-tab.svg │ ├── mozilla-logo-m.svg │ ├── mozilla-logo-black.svg │ └── mozilla-logo-white.svg ├── Procfile ├── travis-scripts ├── python-install.sh └── npm-install.sh ├── bin └── post_compile ├── .eslintrc.json ├── .devcontainer └── devcontainer.json ├── ISSUE.md ├── CODE_OF_CONDUCT.md ├── .vscode └── launch.json ├── .stylelintrc-colors.js ├── .profile ├── .github ├── pull_request_template.md └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── design-to-dev-handoff.md ├── requirements.in ├── webpack.config.js ├── .mergify.yml ├── translation-management.sh ├── tests ├── integration.spec.js └── wait-for-images.js ├── manage.py ├── docs ├── pages.md └── pontoon_integration.md ├── .gitignore ├── docker-compose.yml ├── dev-requirements.txt ├── app.json └── env.default /donate/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.html -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /donate/recaptcha/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /donate/utility/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.9.9 2 | -------------------------------------------------------------------------------- /donate/core/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /donate/payments/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /donate/utility/faker/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /donate/core/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /donate/core/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /donate/core/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /donate/payments/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /donate/recaptcha/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /donate/users/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /donate/core/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /invoke.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | echo: true 3 | -------------------------------------------------------------------------------- /donate/templates/blocks/heading_block.html: -------------------------------------------------------------------------------- 1 |

{{ value }}

2 | -------------------------------------------------------------------------------- /source/sass/base/_utils.scss: -------------------------------------------------------------------------------- 1 | .hidden { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /dockerfiles/Dockerfile.node: -------------------------------------------------------------------------------- 1 | FROM node:14.4-stretch-slim 2 | 3 | WORKDIR /app/ 4 | -------------------------------------------------------------------------------- /donate/users/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'donate.users.apps.UsersConfig' 2 | -------------------------------------------------------------------------------- /dev-requirements.in: -------------------------------------------------------------------------------- 1 | -c requirements.txt 2 | coveralls 3 | flake8 4 | ipython 5 | ptvsd 6 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude= 3 | *migrations* 4 | node_modules 5 | max-line-length=119 6 | -------------------------------------------------------------------------------- /source/images/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaFoundation/donate-wagtail/HEAD/source/images/og.png -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Théo as main contact for string changes 2 | donate/locale/templates/LC_MESSAGES/* @TheoChevalier 3 | -------------------------------------------------------------------------------- /release-steps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Django Migrations 4 | python manage.py migrate --no-input 5 | -------------------------------------------------------------------------------- /source/images/og-image_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaFoundation/donate-wagtail/HEAD/source/images/og-image_2x.png -------------------------------------------------------------------------------- /donate/users/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser 2 | 3 | 4 | class User(AbstractUser): 5 | pass 6 | -------------------------------------------------------------------------------- /source/images/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaFoundation/donate-wagtail/HEAD/source/images/favicon/favicon.ico -------------------------------------------------------------------------------- /donate/payments/exceptions.py: -------------------------------------------------------------------------------- 1 | class InvalidAddress(Exception): 2 | 3 | def __init__(self, errors): 4 | self.errors = errors 5 | -------------------------------------------------------------------------------- /donate/templates/blocks/image_block.html: -------------------------------------------------------------------------------- 1 | {% load wagtailimages_tags %} 2 | {% image value.image original %} 3 |

{{ value.caption }}

4 | -------------------------------------------------------------------------------- /donate/templates/blocks/menu_item.html: -------------------------------------------------------------------------------- 1 | {% include "fragments/menu_item.html" with page=value.page title=value.title has_children=has_children %} 2 | -------------------------------------------------------------------------------- /source/images/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaFoundation/donate-wagtail/HEAD/source/images/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /source/images/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaFoundation/donate-wagtail/HEAD/source/images/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /source/images/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaFoundation/donate-wagtail/HEAD/source/images/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /source/images/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaFoundation/donate-wagtail/HEAD/source/images/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /source/sass/components/_logo-item.scss: -------------------------------------------------------------------------------- 1 | .logo-item { 2 | &__image { 3 | display: block; 4 | width: 75px; 5 | height: 75px; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /donate/users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | name = 'donate.users' 6 | label = 'users' 7 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["stylelint-prettier"], 3 | "rules": { 4 | "prettier/prettier": true, 5 | "color-hex-length": "long" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /donate/core/factory/__init__.py: -------------------------------------------------------------------------------- 1 | from . import core_pages 2 | 3 | __all__ = ['generate'] 4 | 5 | 6 | def generate(seed): 7 | core_pages.generate(seed) 8 | -------------------------------------------------------------------------------- /maintenance/static/_images/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaFoundation/donate-wagtail/HEAD/maintenance/static/_images/favicon/favicon.ico -------------------------------------------------------------------------------- /source/images/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaFoundation/donate-wagtail/HEAD/source/images/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /source/images/favicon/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaFoundation/donate-wagtail/HEAD/source/images/favicon/android-chrome-256x256.png -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | release: ./release-steps.sh 2 | web: cd donate && gunicorn donate.wsgi:application 3 | worker: python manage.py rqworker default wagtail_localize_pontoon.sync 4 | -------------------------------------------------------------------------------- /maintenance/static/_images/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaFoundation/donate-wagtail/HEAD/maintenance/static/_images/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /maintenance/static/_images/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaFoundation/donate-wagtail/HEAD/maintenance/static/_images/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /travis-scripts/python-install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -x # echo commands before running them 4 | 5 | pip install -r requirements.txt -r dev-requirements.txt 6 | -------------------------------------------------------------------------------- /donate/templates/fragments/donate_form_fundraise_up.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /donate/templates/pages/base_page.html: -------------------------------------------------------------------------------- 1 | {% extends "pages/base_page_master.html" %} 2 | 3 | {% block extra_header_classes %} 4 | header__fundraise-up 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /maintenance/static/_images/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaFoundation/donate-wagtail/HEAD/maintenance/static/_images/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /source/sass/components/_rich-text.scss: -------------------------------------------------------------------------------- 1 | .rich-text { 2 | ul { 3 | list-style: inside disc; 4 | } 5 | 6 | ol { 7 | list-style: inside decimal; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /donate/users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | 4 | from .models import User 5 | 6 | admin.site.register(User, UserAdmin) 7 | -------------------------------------------------------------------------------- /travis-scripts/npm-install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -x # echo commands before running them 4 | 5 | npm install -g npm@latest # Needed to use npm ci 6 | npm ci # Use package-lock.json 7 | -------------------------------------------------------------------------------- /donate/templates/payment/card.html: -------------------------------------------------------------------------------- 1 | {% extends "payment/card_master.html" %} 2 | 3 | {# There are no overrides in this template, but alternative apps can template-overide this file (e.g. thunderbird) #} 4 | -------------------------------------------------------------------------------- /source/js/components/analytics.js: -------------------------------------------------------------------------------- 1 | export default function gaEvent(params) { 2 | if (typeof ga === "function") { 3 | params["transport"] = "beacon"; 4 | ga("send", "event", params); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /source/sass/components/_image-feature.scss: -------------------------------------------------------------------------------- 1 | .image-feature { 2 | margin-bottom: $gutter; 3 | 4 | &__image { 5 | display: block; 6 | width: 100%; 7 | height: auto; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /donate/core/management/commands/makemessages.py: -------------------------------------------------------------------------------- 1 | from django.core.management.commands import makemessages 2 | 3 | 4 | class Command(makemessages.Command): 5 | msgmerge_options = ['--no-fuzzy-matching'] 6 | -------------------------------------------------------------------------------- /donate/templates/fragments/messages.html: -------------------------------------------------------------------------------- 1 | {% if messages %} 2 | {% for message in messages %} 3 |
{{ message }}
4 | {% endfor %} 5 | {% endif %} 6 | -------------------------------------------------------------------------------- /donate/templates/payment/thank_you.html: -------------------------------------------------------------------------------- 1 | {% extends "payment/thank_you_master.html" %} 2 | 3 | {# There are no overrides in this template, but alternative apps can template-overide this file (e.g. thunderbird) #} 4 | -------------------------------------------------------------------------------- /donate/templates/payment/card_upsell.html: -------------------------------------------------------------------------------- 1 | {% extends "payment/card_upsell_master.html" %} 2 | 3 | {# There are no overrides in this template, but alternative apps can template-overide this file (e.g. thunderbird) #} 4 | -------------------------------------------------------------------------------- /donate/templates/fragments/ga_events.html: -------------------------------------------------------------------------------- 1 | {% if events %}{{ events|json_script:"ga-events" }}{% endif %} 2 | 3 | {% if datalayer_event %} 4 | {{ datalayer_event|json_script:"datalayer-event" }} 5 | {% endif %} 6 | 7 | 8 | -------------------------------------------------------------------------------- /donate/templates/payment/paypal_upsell.html: -------------------------------------------------------------------------------- 1 | {% extends "payment/paypal_upsell_master.html" %} 2 | 3 | {# There are no overrides in this template, but alternative apps can template-overide this file (e.g. thunderbird) #} 4 | -------------------------------------------------------------------------------- /donate/settings/salesforce.py: -------------------------------------------------------------------------------- 1 | from .environment import env 2 | 3 | 4 | class Salesforce(object): 5 | SALESFORCE_ORGID = env('SALESFORCE_ORGID') 6 | SALESFORCE_CASE_RECORD_TYPE_ID = env('SALESFORCE_CASE_RECORD_TYPE_ID') 7 | -------------------------------------------------------------------------------- /donate/templates/payment/newsletter_signup.html: -------------------------------------------------------------------------------- 1 | {% extends "payment/newsletter_signup_master.html" %} 2 | 3 | {# There are no overrides in this template, but alternative apps can template-overide this file (e.g. thunderbird) #} 4 | -------------------------------------------------------------------------------- /donate/templates/pages/core/ways_to_give_page.html: -------------------------------------------------------------------------------- 1 | {% extends "pages/core/ways_to_give_page_master.html" %} 2 | 3 | {# There are no overrides in this template, but alternative apps can template-overide this file (e.g. thunderbird) #} 4 | -------------------------------------------------------------------------------- /donate/thunderbird/templates/pages/core/campaign_page.html: -------------------------------------------------------------------------------- 1 | {% extends "pages/core/campaign_page_master.html" %} 2 | 3 | {# There are no overrides in this template, but alternative apps can template-overide this file (e.g. thunderbird) #} 4 | -------------------------------------------------------------------------------- /donate/templates/pages/core/contributor_support_page.html: -------------------------------------------------------------------------------- 1 | {% extends "pages/core/contributor_support_page_master.html" %} 2 | 3 | {# There are no overrides in this template, but alternative apps can template-overide this file (e.g. thunderbird) #} 4 | -------------------------------------------------------------------------------- /donate/core/templatetags/auth_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.conf import settings 3 | 4 | register = template.Library() 5 | 6 | 7 | @register.simple_tag 8 | def use_conventional_auth(): 9 | return settings.USE_CONVENTIONAL_AUTH 10 | -------------------------------------------------------------------------------- /donate/core/constants.py: -------------------------------------------------------------------------------- 1 | # Mapping of locales that Babel doesn't recognise to fallbacks that it does recognise 2 | 3 | LOCALE_MAP = { 4 | 'ach': 'en', 5 | 'es-XL': 'es', 6 | 'lg': 'en', 7 | 'lo': 'en', 8 | 'ms': 'en', 9 | 'uk': 'en', 10 | } 11 | -------------------------------------------------------------------------------- /source/js/components/recaptcha.js: -------------------------------------------------------------------------------- 1 | export default function expectRecaptcha(callback) { 2 | if (!window.grecaptcha) { 3 | return setTimeout(() => { 4 | expectRecaptcha(callback); 5 | }, 100); 6 | } 7 | 8 | window.grecaptcha.ready(callback); 9 | } 10 | -------------------------------------------------------------------------------- /source/sass/components/_introduction.scss: -------------------------------------------------------------------------------- 1 | .introduction { 2 | @include font-size(m); 3 | line-height: 1.4; 4 | margin-bottom: ($gutter * 1.5); 5 | 6 | @include media-query(tablet-portrait) { 7 | @include rem-font-size(20); // one off size 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /donate/utility/faker/helpers.py: -------------------------------------------------------------------------------- 1 | import factory 2 | import random 3 | 4 | 5 | # reseed the Faker RNG used by factory using seed 6 | def reseed(seed): 7 | random.seed(seed) 8 | faker = factory.faker.Faker._get_faker(locale='en-US') 9 | faker.random.seed(seed) 10 | -------------------------------------------------------------------------------- /bin/post_compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euox pipefail 4 | 5 | cd $BUILD_DIR 6 | 7 | # Get the po files from S3 8 | curl -o translations.tar https://donate-wagtail-translations.s3.amazonaws.com/translations.tar 9 | 10 | # Untar the archive 11 | tar -C donate -xvf translations.tar 12 | -------------------------------------------------------------------------------- /donate/thunderbird/templates/fragments/donate_form.html: -------------------------------------------------------------------------------- 1 | {% extends "fragments/donate_form.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block minimum_amount_message %} 6 | {% blocktrans with min_amount=minimum_amount trimmed %}Minimum amount is {{ min_amount }}{% endblocktrans %} 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "prettier" 4 | ], 5 | "plugins": [ 6 | "prettier" 7 | ], 8 | "parserOptions": { 9 | "sourceType": "module" 10 | }, 11 | "env": { 12 | "es6": true 13 | }, 14 | "rules": { 15 | "prettier/prettier": "error" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /source/sass/components/_accordion.scss: -------------------------------------------------------------------------------- 1 | .accordion { 2 | margin-bottom: ($gutter * 2); 3 | 4 | &__heading { 5 | @include font-size(xl); 6 | text-align: center; 7 | margin-bottom: ($gutter * 2); 8 | } 9 | 10 | &__items { 11 | border-top: 1px solid $color--border; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /source/sass/components/_support-form.scss: -------------------------------------------------------------------------------- 1 | .support-form { 2 | max-width: 500px; 3 | min-height: 30vh; 4 | 5 | p { 6 | @include font-size(l); 7 | } 8 | 9 | .privacy-notice { 10 | margin-bottom: 1em; 11 | } 12 | 13 | fieldset { 14 | border: none; 15 | padding: 0; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /donate/templates/forms/_field.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{ field }} 4 | 5 | {% if widget_type == 'checkbox_input' %} 6 | {% include "forms/_label.html" %} 7 | {% endif %} 8 | 9 |
10 | -------------------------------------------------------------------------------- /donate/thunderbird/templates/payment/paypal_upsell.html: -------------------------------------------------------------------------------- 1 | {% extends "payment/paypal_upsell_master.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block sustaining_supporter %} 6 | {% trans "We’d love to have you as a sustaining supporter of Thunderbird. Could you add a monthly donation starting next month?" %} 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mozilla Foundation donate", 3 | "dockerFile": "../dockerfiles/Dockerfile.python", 4 | "context": "..", 5 | "extensions": [ 6 | "ms-python.python" 7 | ], 8 | "settings": { 9 | "terminal.integrated.shell.linux": "bash", 10 | "python.linting.flake8Path": "flake8" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /source/sass/components/_card.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | padding: 20px; 3 | background-color: $color--tertiary; 4 | transition: background-color $transition; 5 | 6 | &:hover { 7 | background-color: darken($color--tertiary, 10%); 8 | } 9 | } 10 | 11 | .card-with-image { 12 | &:only-child { 13 | max-width: 260px; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /dockerfiles/Dockerfile.python: -------------------------------------------------------------------------------- 1 | FROM python:3.9.9-slim 2 | 3 | RUN apt-get update && apt-get install -y \ 4 | gettext \ 5 | git \ 6 | --no-install-recommends && rm -rf /var/lib/apt/lists/* 7 | 8 | # We want output 9 | ENV PYTHONUNBUFFERED 1 10 | # We don't want *.pyc 11 | ENV PYTHONDONTWRITEBYTECODE 1 12 | 13 | WORKDIR /app/ 14 | -------------------------------------------------------------------------------- /donate/settings/database.py: -------------------------------------------------------------------------------- 1 | from .environment import env 2 | 3 | 4 | class Database(object): 5 | 6 | @property 7 | def DATABASES(self): 8 | config = { 9 | 'default': env.db_url_config(env('DATABASE_URL')) 10 | } 11 | config['default']['ATOMIC_REQUESTS'] = True 12 | 13 | return config 14 | -------------------------------------------------------------------------------- /source/images/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #ffffff 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /source/sass/components/_app.scss: -------------------------------------------------------------------------------- 1 | .app { 2 | &--rtl { 3 | direction: rtl; 4 | } 5 | 6 | &__notice-bar { 7 | background: $color--grey-05; 8 | text-align: center; 9 | 10 | h3 { 11 | font-family: "Nunito Sans", Helvetica, Arial, sans-serif; 12 | margin: 0; 13 | padding: 8px 0; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /donate/templates/forms/_label.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /donate/payments/wagtail_hooks.py: -------------------------------------------------------------------------------- 1 | from wagtail.core import hooks 2 | from wagtail_ab_testing.events import BaseEvent 3 | 4 | 5 | class VisitThankYouPageEvent(BaseEvent): 6 | name = "Visit the thank you page" 7 | requires_page = False 8 | 9 | 10 | @hooks.register('register_ab_testing_event_types') 11 | def register_submit_form_event_type(): 12 | return { 13 | 'visit-thank-you-page': VisitThankYouPageEvent(), 14 | } 15 | -------------------------------------------------------------------------------- /donate/templates/fragments/menu_item.html: -------------------------------------------------------------------------------- 1 | {% load wagtailcore_tags %}{% pageurl page as item_url %} 2 | 6 | {{ title|default:page.title }} 7 | {% if has_children %} 8 | 9 | {% endif %} 10 | 11 | -------------------------------------------------------------------------------- /ISSUE.md: -------------------------------------------------------------------------------- 1 | **I'm submitting a ...** 2 | - [ ] bug report 3 | - [ ] feature request 4 | 5 | 6 | **What is the current behavior?** 7 | 8 | 9 | *(If the current behavior is a bug, please provide the steps to reproduce)* 10 | 11 | 12 | **What is the expected behavior?** 13 | 14 | 15 | **If this is a bug report, please tell us about your environment:** 16 | 17 | - OS: 18 | - Browser: 19 | - Browser Version: 20 | 21 | **Other information:** 22 | -------------------------------------------------------------------------------- /source/js/components/env.js: -------------------------------------------------------------------------------- 1 | export default function fetchEnv(handleEnvData) { 2 | let envReq = new XMLHttpRequest(); 3 | 4 | envReq.addEventListener("load", () => { 5 | let envData = {}; 6 | 7 | try { 8 | envData = JSON.parse(envReq.response); 9 | } catch (e) { 10 | // discard 11 | } 12 | 13 | handleEnvData(envData); 14 | }); 15 | 16 | envReq.open("GET", "/environment.json"); 17 | envReq.send(); 18 | } 19 | -------------------------------------------------------------------------------- /source/sass/components/_error.scss: -------------------------------------------------------------------------------- 1 | .error { 2 | padding-top: ($gutter * 6); 3 | padding-bottom: ($gutter * 6); 4 | 5 | &__message { 6 | margin-bottom: ($gutter * 2); 7 | color: $color--black; 8 | } 9 | 10 | &__image { 11 | width: 100%; 12 | height: auto; 13 | max-width: 500px; 14 | margin-bottom: ($gutter * 2); 15 | } 16 | } 17 | 18 | .error-message { 19 | @include font-size(xxs); 20 | color: $color--warning; 21 | } 22 | -------------------------------------------------------------------------------- /donate/payments/__init__.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | import braintree 4 | 5 | gateway = braintree.BraintreeGateway( 6 | braintree.Configuration( 7 | braintree.Environment.Sandbox if settings.BRAINTREE_USE_SANDBOX else braintree.Environment.Production, 8 | merchant_id=settings.BRAINTREE_MERCHANT_ID, 9 | public_key=settings.BRAINTREE_PUBLIC_KEY, 10 | private_key=settings.BRAINTREE_PRIVATE_KEY 11 | ) 12 | ) 13 | -------------------------------------------------------------------------------- /source/images/mozilla-logo-m.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /source/sass/abstracts/_functions.scss: -------------------------------------------------------------------------------- 1 | // Strip the unit from the given value and return the value 2 | @function strip-unit($value) { 3 | @return $value / ($value * 0 + 1); 4 | } 5 | 6 | // Return an em unit based on the pixel value and context 7 | @function rem($px, $context: $base-font-size) { 8 | @return #{strip-unit($px / strip-unit($context))}rem; 9 | } 10 | 11 | // Map z-index keys 12 | @function z-index($key) { 13 | @return map-get($z-index, $key); 14 | } 15 | -------------------------------------------------------------------------------- /maintenance/static/_images/mozilla-logo-m.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /donate/templates/fragments/campaign_page_fundraiseup_form.html: -------------------------------------------------------------------------------- 1 | {% load static i18n %} 2 | 3 |
4 | 12 |
-------------------------------------------------------------------------------- /donate/thunderbird/templates/pages/core/landing_page.html: -------------------------------------------------------------------------------- 1 | {% extends "pages/core/landing_page_master.html" %} 2 | 3 | {% load i18n wagtailimages_tags %} 4 | 5 | {% block notice_bar %} 6 | {% if request.GET.tbdownload == 'true' %} 7 |

{% blocktrans with url='https://www.thunderbird.net/download/' %}Your download should have begun automatically. If it didn’t work, try downloading again here.{% endblocktrans %}

8 | {% endif %} 9 | {% endblock %} 10 | 11 | -------------------------------------------------------------------------------- /source/sass/components/_icon.scss: -------------------------------------------------------------------------------- 1 | .icon { 2 | width: 20px; 3 | height: 20px; 4 | transition: fill $transition; 5 | fill: $color--white; 6 | 7 | &:hover { 8 | fill: darken($color--white, 20%); 9 | } 10 | 11 | &--home { 12 | width: 15px; 13 | height: 15px; 14 | margin-right: 5px; 15 | fill: $color--primary; 16 | } 17 | 18 | &--footer-social { 19 | @include media-query(tablet-portrait) { 20 | margin-right: 10px; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # 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 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 9 | -------------------------------------------------------------------------------- /donate/core/management/commands/enqueue_pontoon_sync.py: -------------------------------------------------------------------------------- 1 | """ 2 | Management command that trigger a sync with Pontoon. Used on Heroku as a scheduled task. 3 | """ 4 | from django.core.management.base import BaseCommand 5 | 6 | from donate.core.pontoon import CustomSyncManager 7 | 8 | 9 | class Command(BaseCommand): 10 | help = 'Enqueue a `sync_pontoon` job.' 11 | 12 | def handle(self, *args, **options): 13 | print("Syncing with Pontoon...") 14 | CustomSyncManager().trigger() 15 | print("Done!") 16 | -------------------------------------------------------------------------------- /donate/core/templatetags/analytics_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.inclusion_tag('fragments/ga_events.html', takes_context=True) 7 | # This tag is used to render GA or datalayer event data on the frontend, 8 | # so it can be picked up by JS. 9 | def render_ga_event_data(context): 10 | return { 11 | 'events': context['request'].session.pop('ga_events', []), 12 | 'datalayer_event': context['request'].session.pop('datalayer_event', {}) 13 | } 14 | -------------------------------------------------------------------------------- /donate/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for donate 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.8/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from configurations.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "donate.settings") 15 | os.environ.setdefault("DJANGO_CONFIGURATION", "Development") 16 | 17 | application = get_wsgi_application() 18 | -------------------------------------------------------------------------------- /source/js/components/donation-currency-width.js: -------------------------------------------------------------------------------- 1 | class DonationCurrencyWidth { 2 | static selector() { 3 | return "[data-donation-currency]"; 4 | } 5 | 6 | constructor(node) { 7 | this.node = node; 8 | this.getWidth(); 9 | } 10 | 11 | getWidth() { 12 | this.currencyWidth = this.node.getBoundingClientRect(); 13 | document.documentElement.style.setProperty( 14 | "--currency-width", 15 | this.currencyWidth.width + "px" 16 | ); 17 | } 18 | } 19 | 20 | export default DonationCurrencyWidth; 21 | -------------------------------------------------------------------------------- /donate/users/migrations/0002_auto_20210602_1559.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.11 on 2021-06-02 15: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.AlterField( 14 | model_name='user', 15 | name='first_name', 16 | field=models.CharField(blank=True, max_length=150, verbose_name='first name'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version ":"0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "[django:docker] runserver", 6 | "type": "python", 7 | "request": "attach", 8 | "pathMappings": [ 9 | { 10 | "localRoot":"${workspaceFolder}", 11 | "remoteRoot": "/app" 12 | } 13 | ], 14 | "port": 8001, 15 | "host": "localhost", 16 | "redirectOutput": true 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /donate/thunderbird/templates/payment/newsletter_signup.html: -------------------------------------------------------------------------------- 1 | {% extends "payment/newsletter_signup_master.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block thank-you %} 6 |

{% trans "Thank you for your generous gift" %}

7 |

8 | {% blocktrans trimmed %} 9 | We’re working hard to protect your inbox. Sign up to receive email messages concerning Thunderbird, including updates and requests for feedback. 10 | {% endblocktrans %} 11 | 12 |

13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /.stylelintrc-colors.js: -------------------------------------------------------------------------------- 1 | var config = { 2 | "plugins": ["stylelint-prettier"], 3 | "rules": { 4 | "color-named": "never", 5 | "color-no-hex": true, 6 | "declaration-property-value-disallowed-list": [ 7 | { 8 | "/.*/": [ 9 | /rgba{0,1}\(/i, 10 | /hsla{0,1}\(/i, 11 | /hwb\(/i, 12 | /gray\(/i 13 | ] 14 | }, 15 | { 16 | "message": "Custom colors are not allowed. Please use brand colors listed in _variables.scss." 17 | } 18 | ] 19 | } 20 | }; 21 | 22 | module.exports = config; 23 | -------------------------------------------------------------------------------- /source/images/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mozilla Donate", 3 | "short_name": "Mozilla Donate", 4 | "icons": [ 5 | { 6 | "src": "/_images/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/_images/android-chrome-256x256.png", 12 | "sizes": "256x256", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /donate/templates/fragments/campaign_page_intro.html: -------------------------------------------------------------------------------- 1 | {% load static wagtailcore_tags i18n %} 2 | 3 |
4 |
5 | {% if page.intro_header %} 6 |

7 | {% trans page.intro_header context "Header" %} 8 |

9 | {% endif %} 10 |
11 | {{ page.intro|richtext }} 12 |
13 |
14 |
-------------------------------------------------------------------------------- /maintenance/static/_images/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mozilla Donate", 3 | "short_name": "Mozilla Donate", 4 | "icons": [ 5 | { 6 | "src": "/_images/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/_images/android-chrome-256x256.png", 12 | "sizes": "256x256", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /donate/core/management/commands/clear_fake_data.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from wagtail.core.models import Page 4 | from wagtail.images.models import Image 5 | 6 | 7 | class Command(BaseCommand): 8 | help = 'Delete all Page and Image objects from the database along with associated media files.' 9 | 10 | def handle(self, *args, **options): 11 | Page.objects.all().delete() 12 | self.stdout.write('Deleted all pages') 13 | num_deleted, __ = Image.objects.all().delete() 14 | self.stdout.write(f'Deleted {num_deleted} images') 15 | -------------------------------------------------------------------------------- /.profile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Create the necessary config files to be able to push to GitHub from Heroku. 3 | # Used for syncing translation files between Pontoon and wagtail-donate-prod 4 | 5 | set -e 6 | 7 | if [ $USE_PONTOON ]; then 8 | echo "Generating SSH config" 9 | SSH_DIR=/app/.ssh 10 | 11 | mkdir -p $SSH_DIR 12 | chmod 700 $SSH_DIR 13 | 14 | # echo is messing with the newlines, using this instead: 15 | cat > $SSH_DIR/id_rsa << EOF 16 | $SSH_KEY 17 | EOF 18 | chmod 400 $SSH_DIR/id_rsa 19 | 20 | echo $SSH_CONFIG > $SSH_DIR/config 21 | chmod 600 $SSH_DIR/config 22 | echo "Done!" 23 | fi 24 | -------------------------------------------------------------------------------- /source/sass/components/_column-spacing.scss: -------------------------------------------------------------------------------- 1 | // Component to add spacing to flex based layout elements without causing them to go over 100% total width 2 | .column-spacing { 3 | .layout__secondary-col & { 4 | @include media-query(tablet-landscape) { 5 | padding-right: ($gutter * 3); 6 | } 7 | } 8 | 9 | // Move padding to opposite side for RTL languages 10 | // sass-lint:disable force-element-nesting 11 | .app--rtl .layout__secondary-col & { 12 | @include media-query(tablet-landscape) { 13 | padding-right: 0; 14 | padding-left: ($gutter * 3); 15 | } 16 | } 17 | // sass-lint:enddisable 18 | } 19 | -------------------------------------------------------------------------------- /source/sass/components/_link.scss: -------------------------------------------------------------------------------- 1 | .link { 2 | $root: &; 3 | 4 | &--light-bg { 5 | color: $color--link; 6 | 7 | &:hover, 8 | &:active, 9 | &:focus { 10 | color: $color--link-hover; 11 | } 12 | 13 | #{$root}--icon { 14 | fill: $color--black; 15 | } 16 | } 17 | 18 | &--dark-bg { 19 | color: $color--white; 20 | 21 | &:hover, 22 | &:active, 23 | &:focus { 24 | color: $color--light-blue; 25 | } 26 | 27 | &#{$root}--icon { 28 | fill: $color--white; 29 | } 30 | } 31 | 32 | &--icon { 33 | #{$root}__svg-title { 34 | @include hidden(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /source/sass/components/_page-header.scss: -------------------------------------------------------------------------------- 1 | .page-header { 2 | background-color: $color--black; 3 | padding: ($gutter * 3) $gutter; 4 | 5 | @include media-query(tablet-portrait) { 6 | padding: ($gutter * 4) $gutter; 7 | } 8 | 9 | &__container { 10 | max-width: $site-width--default; 11 | text-align: center; 12 | 13 | @include media-query(tablet-portrait) { 14 | padding: 0 ($gutter * 4); 15 | } 16 | 17 | @include media-query(desktop) { 18 | padding: 0; 19 | margin: 0 auto; 20 | } 21 | } 22 | 23 | &__heading { 24 | @include font-size(xxl); 25 | margin: 0; 26 | color: $color--white; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Closes # 2 | Related PRs/issues # 3 | 4 | ## Checklist 5 | 6 | _Remove unnecessary checks_ 7 | 8 | **Tests** 9 | - [ ] Is the code I'm adding covered by tests? 10 | 11 | **Changes in Models:** 12 | - [ ] Did I update or add new fake data? 13 | - [ ] Did I squash my migration? 14 | - [ ] [Are my changes backward-compatible](https://github.com/mozilla/foundation.mozilla.org/blob/main/docs/workflow.md#django-migrations-what-to-do-when-working-on-backward-incompatible-migrations). If not, did I schedule a deploy with the rest of the team? 15 | 16 | **Documentation:** 17 | - [ ] Is my code documented? 18 | - [ ] Did I update the documentation? 19 | -------------------------------------------------------------------------------- /donate/recaptcha/fields.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.core.exceptions import ValidationError 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | from .utils import verify 6 | 7 | 8 | class ReCaptchaField(forms.CharField): 9 | widget = forms.HiddenInput 10 | 11 | def __init__(self, *args, **kwargs): 12 | super().__init__() 13 | self.required = True 14 | self.recaptcha_secret = kwargs.get('secret', '') 15 | 16 | def validate(self, value): 17 | super().validate(value) 18 | if not verify(value, self.recaptcha_secret): 19 | raise ValidationError(_("Captcha was invalid. Please try again.")) 20 | -------------------------------------------------------------------------------- /donate/settings/sentry.py: -------------------------------------------------------------------------------- 1 | from .environment import env 2 | 3 | 4 | class Sentry(object): 5 | SENTRY_DSN = env('SENTRY_DSN') 6 | SENTRY_ENVIRONMENT = env('SENTRY_ENVIRONMENT') 7 | HEROKU_RELEASE_VERSION = env('HEROKU_RELEASE_VERSION') 8 | 9 | @classmethod 10 | def pre_setup(cls): 11 | super().pre_setup() 12 | 13 | import sentry_sdk 14 | from sentry_sdk.integrations.django import DjangoIntegration 15 | sentry_sdk.init( 16 | dsn=cls.SENTRY_DSN, 17 | integrations=[DjangoIntegration()], 18 | release=cls.HEROKU_RELEASE_VERSION, 19 | environment=cls.SENTRY_ENVIRONMENT 20 | ) 21 | -------------------------------------------------------------------------------- /donate/templates/payment/includes/trigger_ab_testing_thank_you_event.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /donate/core/translation.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import gettext_lazy as _ 2 | 3 | """ 4 | A series of strings that are used by Django core, that we want to be able 5 | to provide translations for in the languages we support. 6 | 7 | Declaring strings here will ensure that they are included in our translation 8 | files, and used in preference to Django's. 9 | """ 10 | 11 | # Field.default_error_messages['required'] 12 | default_required_message = _('This field is required.') 13 | 14 | # DecimalField.default_error_messages['invalid'] 15 | decimal_invalid = _('Enter a number.') 16 | 17 | # EmailValidator.message 18 | email_validator_message = _('Enter a valid email address.') 19 | -------------------------------------------------------------------------------- /source/sass/components/_sticky-message.scss: -------------------------------------------------------------------------------- 1 | .sticky-message { 2 | @include z-index(sticky-message); 3 | position: fixed; 4 | bottom: 0; 5 | width: 100%; 6 | transition: opacity $transition; 7 | 8 | @include media-query(tablet-portrait) { 9 | display: none; 10 | } 11 | 12 | &.hidden { 13 | opacity: 0; 14 | pointer-events: none; 15 | } 16 | 17 | &__container { 18 | padding: $gutter; 19 | border-top: 1px solid $color--border; 20 | background-color: $color--white; 21 | } 22 | 23 | &__action { 24 | display: block; 25 | width: 100%; 26 | font-weight: $weight--bold; 27 | text-align: center; 28 | scroll-behavior: smooth; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /donate/templates/wagtailadmin/login.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/login.html" %} 2 | {% load auth_tags %} 3 | 4 | {% block login_form %} 5 | 6 | {% use_conventional_auth as conventional_auth_enabled %} 7 | 8 | {% if conventional_auth_enabled %} 9 | 10 | {{ block.super }} 11 | 12 | {% else %} 13 | 14 |

Admin

15 | 16 | 17 | 18 | Sign in with Mozilla SSO 19 | 20 | 21 |

If you lack SSO access, please ask your contact within Mozilla.

22 | {% endif %} 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /source/js/components/waypoint-detection.js: -------------------------------------------------------------------------------- 1 | import "intersection-observer"; 2 | import scrollama from "scrollama"; 3 | 4 | function scrollamaInit() { 5 | // instantiate the scrollama 6 | const scroller = scrollama(); 7 | 8 | // setup the instance, pass callback functions 9 | scroller 10 | .setup({ 11 | step: ".js-data-waypoint", 12 | }) 13 | .onStepEnter((response) => { 14 | document 15 | .querySelectorAll("[data-waypoint-element]") 16 | .forEach((stepItem) => { 17 | stepItem.classList.add("hidden"); 18 | }); 19 | }); 20 | } 21 | 22 | export default () => { 23 | if (document.querySelectorAll("[data-waypoint-element]").length) { 24 | scrollamaInit(); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /source/sass/components/_tabs.scss: -------------------------------------------------------------------------------- 1 | .tabs { 2 | &__label { 3 | @include font-size(default); 4 | display: flex; 5 | flex-direction: row; 6 | align-items: center; 7 | padding: $gutter 0; 8 | text-align: center; 9 | font-weight: $weight--bold; 10 | letter-spacing: 1.25px; 11 | line-height: 1; 12 | color: $color--black; 13 | margin-right: ($gutter); 14 | cursor: pointer; 15 | } 16 | 17 | &__label-text { 18 | @include media-query(tablet-landscape) { 19 | position: relative; 20 | top: 2px; 21 | } 22 | } 23 | 24 | &__radio-option { 25 | margin-right: ($gutter / 2); 26 | } 27 | 28 | &__panel { 29 | &--hidden { 30 | display: none; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /donate/core/templatetags/form_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | from wagtail.core.utils import camelcase_to_underscore 4 | 5 | register = template.Library() 6 | 7 | 8 | @register.filter 9 | def widget_type(bound_field): 10 | return camelcase_to_underscore(bound_field.field.widget.__class__.__name__) 11 | 12 | 13 | @register.filter 14 | def field_type(bound_field): 15 | return camelcase_to_underscore(bound_field.field.__class__.__name__) 16 | 17 | 18 | @register.inclusion_tag('forms/form_field.html') 19 | def render_form_field(field, css_classes=False, front_end_validated=False): 20 | return { 21 | 'field': field, 22 | 'css_classes': css_classes, 23 | 'front_end_validated': front_end_validated 24 | } 25 | -------------------------------------------------------------------------------- /source/js/components/copy-url.js: -------------------------------------------------------------------------------- 1 | class CopyURL { 2 | static selector() { 3 | return "[data-copy-link]"; 4 | } 5 | 6 | constructor(node) { 7 | this.button = node; 8 | this.input = this.button.querySelector("[data-copy-value]"); 9 | this.bindEvents(); 10 | } 11 | 12 | copyText() { 13 | this.input.select(); 14 | this.input.setSelectionRange(0, 99999); /*For mobile devices*/ 15 | document.execCommand("copy"); 16 | } 17 | 18 | updateButton() { 19 | this.button.classList.add("copied"); 20 | } 21 | 22 | bindEvents() { 23 | this.button.addEventListener("click", (e) => { 24 | e.preventDefault(); 25 | this.copyText(); 26 | this.updateButton(); 27 | }); 28 | } 29 | } 30 | 31 | export default CopyURL; 32 | -------------------------------------------------------------------------------- /source/sass/components/_logo-showcase.scss: -------------------------------------------------------------------------------- 1 | .logo-showcase { 2 | margin-bottom: $gutter; 3 | &__container { 4 | display: flex; 5 | flex-direction: row; 6 | flex-wrap: wrap; 7 | } 8 | 9 | &__item { 10 | margin-right: $gutter; 11 | } 12 | 13 | .app--landing-page & { 14 | margin-bottom: 0; 15 | &::after { 16 | position: relative; 17 | left: -($gutter); 18 | right: -($gutter); 19 | display: block; 20 | content: ""; 21 | width: calc(100% + (#{$gutter} * 2)); 22 | border-bottom: 1px solid $color--border; 23 | padding-top: ($gutter * 1.5); 24 | margin-bottom: $gutter; 25 | 26 | @include media-query(tablet-portrait) { 27 | display: none; 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /donate/core/feature_flags.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from wagtail.admin.edit_handlers import FieldPanel, TabbedInterface, ObjectList 4 | from wagtail.contrib.settings.models import BaseSetting, register_setting 5 | 6 | 7 | @register_setting(icon="tick") 8 | class FeatureFlags(BaseSetting): 9 | 10 | enable_upsell_view = models.BooleanField( 11 | default=False, 12 | verbose_name="Enable the upsell view", 13 | help_text="Checking this will allow the upsell view to be displayed after one-time donations", 14 | ) 15 | 16 | content_panels = [FieldPanel("enable_upsell_view")] 17 | 18 | edit_handler = TabbedInterface( 19 | [ 20 | ObjectList(content_panels, heading="Feature Flags"), 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /donate/recaptcha/utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import requests 4 | from requests.exceptions import RequestException 5 | 6 | API_URL = 'https://www.google.com/recaptcha/api/siteverify' 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | def verify(token, secret): 11 | # this is only used for the (card based) donate form, 12 | # as verifying in donate.recaptcha.fields.ReCaptchaField 13 | try: 14 | response = requests.post(API_URL, timeout=5, data={ 15 | 'secret': secret, 16 | 'response': token 17 | }) 18 | response.raise_for_status() 19 | except RequestException: 20 | logger.exception('Failed to make request to recaptcha API') 21 | return True 22 | 23 | return response.json()['success'] 24 | -------------------------------------------------------------------------------- /donate/core/migrations/0003_auto_20230531_2044.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.14 on 2023-05-31 20:44 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('core', '0002_featureflags'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='campaignpage', 15 | name='cta_first', 16 | field=models.BooleanField(default=False, help_text='Check this to shift the CTA to the top on mobile'), 17 | ), 18 | migrations.AddField( 19 | model_name='campaignpage', 20 | name='intro_header', 21 | field=models.CharField(blank=True, default='Donate now', max_length=200), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /donate/templates/base.html: -------------------------------------------------------------------------------- 1 | {% extends "base_master.html" %} 2 | 3 | 4 | {% block fundraiseup_script %} 5 | 6 | 13 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /source/sass/components/_nav-item.scss: -------------------------------------------------------------------------------- 1 | .nav-item { 2 | $root: &; 3 | 4 | &--primary { 5 | @include font-size(m); 6 | font-weight: $weight--bold; 7 | 8 | @include media-query(tablet-landscape) { 9 | padding-right: ($grid); 10 | } 11 | 12 | #{$root}__link { 13 | color: $color--white; 14 | 15 | @include media-query(tablet-landscape) { 16 | color: $color--black; 17 | } 18 | 19 | &:hover, 20 | &:active, 21 | &:focus { 22 | text-decoration: underline; 23 | } 24 | } 25 | } 26 | 27 | .primary-nav & { 28 | #{$root}__link { 29 | pointer-events: none; 30 | } 31 | } 32 | 33 | .primary-nav.is-visible & { 34 | #{$root}__link { 35 | pointer-events: auto; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /donate/core/utils.py: -------------------------------------------------------------------------------- 1 | from wagtail.core.models import Page 2 | 3 | 4 | def is_donation_page(page_id): 5 | from .models import CampaignPage, LandingPage # Avoid circular import 6 | try: 7 | page = Page.objects.live().get(pk=page_id).specific 8 | except Page.DoesNotExist: 9 | return False 10 | 11 | return page.__class__ in [CampaignPage, LandingPage] 12 | 13 | 14 | def queue_ga_event(request, event_data): 15 | if 'ga_events' in request.session: 16 | request.session['ga_events'].append(event_data) 17 | request.session.modified = True 18 | else: 19 | request.session['ga_events'] = [event_data] 20 | 21 | 22 | def queue_datalayer_event(request, event_data): 23 | request.session['datalayer_event'] = event_data 24 | request.session.modified = True 25 | -------------------------------------------------------------------------------- /source/sass/components/_notification.scss: -------------------------------------------------------------------------------- 1 | .notification { 2 | margin-bottom: ($gutter * 1.5); 3 | 4 | @include media-query(tablet-portrait) { 5 | } 6 | 7 | &--neutral { 8 | background-color: $color--neutral; 9 | } 10 | 11 | &--warning { 12 | background-color: $color--warning; 13 | } 14 | 15 | &--positive { 16 | background-color: $color--positive; 17 | } 18 | 19 | &__container { 20 | display: flex; 21 | flex-direction: column; 22 | justify-content: flex-start; 23 | padding: ($gutter * 1.25) $gutter; 24 | 25 | @include media-query(tablet-portrait) { 26 | flex-direction: row; 27 | padding: $gutter; 28 | } 29 | } 30 | 31 | &__action { 32 | @include media-query(tablet-portrait) { 33 | margin-left: $gutter; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /donate/recaptcha/tests/test_fields.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from django.core.exceptions import ValidationError 4 | from django.test import TestCase 5 | 6 | from ..fields import ReCaptchaField 7 | 8 | 9 | class RecaptchaFieldTestCase(TestCase): 10 | 11 | def test_validation_error_if_token_not_valid(self): 12 | with mock.patch('donate.recaptcha.fields.verify', autospec=True) as mock_verify: 13 | mock_verify.return_value = False 14 | with self.assertRaises(ValidationError): 15 | ReCaptchaField(secret='a-secret').validate('foo') 16 | 17 | def test_validation_ok_if_token_valid(self): 18 | with mock.patch('donate.recaptcha.fields.verify', autospec=True) as mock_verify: 19 | mock_verify.return_value = True 20 | self.assertIsNone(ReCaptchaField().validate('foo')) 21 | -------------------------------------------------------------------------------- /donate/thunderbird/templates/payment/card_upsell.html: -------------------------------------------------------------------------------- 1 | {% extends "payment/card_upsell_master.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block sustaining_supporter %} 6 | {% trans "We’d love to have you as a sustaining supporter of Thunderbird. Could you add a monthly donation starting next month?" %} 7 | {% endblock %} 8 | 9 | {% block form_authorization %} 10 | {% blocktrans with help_url='/help' %}I authorize MZLA Technologies Corporation to automatically charge my card every month in the amount indicated above, starting next month on the same date as today, and continuing each month afterwards until I cancel. I understand that I must cancel at least 5 days before the next scheduled donation by submitting this form, and that I may only request a refund within 15 days from the date of a donation.{% endblocktrans %} 11 | {% endblock %} -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | # Need to install a pre-release in order to get the RedLock.locked() method 2 | babel 3 | basket-client==1.0.0 4 | boto3 5 | braintree 6 | dj-database-url 7 | django-configurations 8 | django-countries 9 | django-csp 10 | django-environ 11 | django-redis 12 | django-rq 13 | django-storages 14 | django==3.1.14 15 | factory-boy 16 | freezegun 17 | # Need to install a pre-release in order to get the RedLock.locked() method 18 | git+https://github.com/glasslion/redlock.git@master#egg=redlock 19 | gunicorn 20 | lxml==4.6.2 21 | mailchimp-marketing==3.0.44 22 | mozilla-django-oidc 23 | psycopg2-binary 24 | pygit2==1.7.1 25 | pysilverpop==0.2.6 26 | requests 27 | rq==1.2.0 28 | scout-apm 29 | sentry-sdk 30 | stripe 31 | wagtail-ab-testing==0.6 32 | wagtail-factories 33 | wagtail-localize==1.0.1 34 | wagtail-localize-git==0.12.0 35 | wagtail==2.15.1 36 | whitenoise 37 | -------------------------------------------------------------------------------- /donate/settings/braintree.py: -------------------------------------------------------------------------------- 1 | from .environment import env 2 | 3 | 4 | class Braintree(object): 5 | BRAINTREE_USE_SANDBOX = env('BRAINTREE_USE_SANDBOX') 6 | BRAINTREE_MERCHANT_ID = env('BRAINTREE_MERCHANT_ID') 7 | BRAINTREE_PUBLIC_KEY = env('BRAINTREE_PUBLIC_KEY') 8 | BRAINTREE_PRIVATE_KEY = env('BRAINTREE_PRIVATE_KEY') 9 | BRAINTREE_TOKENIZATION_KEY = env('BRAINTREE_TOKENIZATION_KEY') 10 | BRAINTREE_MERCHANT_ACCOUNTS = env('BRAINTREE_MERCHANT_ACCOUNTS') 11 | BRAINTREE_PLANS = env('BRAINTREE_PLANS') 12 | BRAINTREE_MERCHANT_ACCOUNTS_PAYPAL_MICRO = env('BRAINTREE_MERCHANT_ACCOUNTS_PAYPAL_MICRO') 13 | 14 | USE_PAYPAL = env('USE_PAYPAL') 15 | 16 | @property 17 | def BRAINTREE_PARAMS(self): 18 | return { 19 | 'use_sandbox': self.BRAINTREE_USE_SANDBOX, 20 | 'token': self.BRAINTREE_TOKENIZATION_KEY, 21 | } 22 | -------------------------------------------------------------------------------- /donate/templates/pages/core/content_page.html: -------------------------------------------------------------------------------- 1 | {% extends "pages/base_page.html" %} 2 | {% load form_tags static wagtailcore_tags wagtailimages_tags i18n %} 3 | 4 | {% block content %} 5 | 6 | 14 | 15 |
16 |
17 |
18 | 19 |
20 | {{ page.body }} 21 |
22 |
23 |
24 |
25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /donate/payments/braintree_webhooks.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.http import HttpResponse, HttpResponseBadRequest 3 | from django.utils.decorators import method_decorator 4 | from django.views.decorators.csrf import csrf_exempt 5 | from django.views.generic import FormView 6 | 7 | from .tasks import queue, process_webhook 8 | 9 | 10 | class WebhookForm(forms.Form): 11 | bt_signature = forms.CharField() 12 | bt_payload = forms.CharField() 13 | 14 | 15 | @method_decorator(csrf_exempt, name='dispatch') 16 | class BraintreeWebhookView(FormView): 17 | form_class = WebhookForm 18 | http_method_names = ['post'] 19 | 20 | def form_valid(self, form): 21 | queue.enqueue(process_webhook, form.cleaned_data, description="Handle Braintree webhook") 22 | return HttpResponse() 23 | 24 | def form_invalid(self, form): 25 | return HttpResponseBadRequest() 26 | -------------------------------------------------------------------------------- /source/js/payments-paypal-upsell.js: -------------------------------------------------------------------------------- 1 | import initPaypal from "./components/paypal"; 2 | 3 | function setupBraintree() { 4 | var paymentForm = document.getElementById("payments__braintree-form"), 5 | nonceInput = document.getElementById("id_braintree_nonce"), 6 | amountInput = document.getElementById("id_amount"), 7 | currencyInput = document.getElementById("id_currency"); 8 | 9 | var getAmount = () => { 10 | return amountInput.value; 11 | }; 12 | var getCurrency = () => currencyInput.value; 13 | var onAuthorize = (payload) => { 14 | nonceInput.value = payload.nonce; 15 | amountInput.value = getAmount(); 16 | paymentForm.submit(); 17 | }; 18 | 19 | initPaypal( 20 | getAmount, 21 | getCurrency, 22 | onAuthorize, 23 | "vault", 24 | "#payments__paypal-button--upsell" 25 | ); 26 | } 27 | 28 | document.addEventListener("DOMContentLoaded", function () { 29 | setupBraintree(); 30 | }); 31 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | let webpack = require(`webpack`); 2 | let path = require(`path`); 3 | let frontendPath = path.resolve(__dirname, `donate`, `frontend`, `_js`); 4 | 5 | let rules = [ 6 | { 7 | test: /\.js(x?)$/, 8 | exclude: /node_modules/, 9 | loader: `babel-loader`, 10 | query: { 11 | presets: [[`@babel/preset-env`, { targets: `> 1%, last 2 versions` }]], 12 | }, 13 | }, 14 | ]; 15 | 16 | function generate(name) { 17 | return { 18 | entry: `./source/js/${name}.js`, 19 | output: { 20 | path: frontendPath, 21 | filename: `${name}.compiled.js`, 22 | }, 23 | module: { 24 | rules, 25 | }, 26 | devtool: `none`, // see https://webpack.js.org/configuration/devtool/ 27 | }; 28 | } 29 | 30 | let configs = [ 31 | `main`, 32 | `payments-card`, 33 | `payments-paypal`, 34 | `payments-paypal-upsell`, 35 | ]; 36 | 37 | module.exports = configs.map((name) => generate(name)); 38 | -------------------------------------------------------------------------------- /donate/payments/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | 6 | app_name = 'payments' 7 | 8 | 9 | urlpatterns = [ 10 | path( 11 | 'card/thank-you/', 12 | views.CardUpsellView.as_view(), 13 | name='card_upsell' 14 | ), 15 | path( 16 | 'card//', 17 | views.CardPaymentView.as_view(), 18 | name='card' 19 | ), 20 | path( 21 | 'paypal/', 22 | views.PaypalPaymentView.as_view(), 23 | name='paypal' 24 | ), 25 | path( 26 | 'paypal/thank-you/', 27 | views.PaypalUpsellView.as_view(), 28 | name='paypal_upsell' 29 | ), 30 | path( 31 | 'stay-in-touch/', 32 | views.NewsletterSignupView.as_view(), 33 | name='newsletter_signup' 34 | ), 35 | path( 36 | 'thank-you/', 37 | views.ThankYouView.as_view(), 38 | name='completed' 39 | ) 40 | ] 41 | -------------------------------------------------------------------------------- /donate/settings/__init__.py: -------------------------------------------------------------------------------- 1 | from .environment import env 2 | from .base import Base 3 | from .braintree import Braintree 4 | from .database import Database 5 | from .redis import Redis 6 | from .secure import Secure 7 | from .sentry import Sentry 8 | from .testing import Testing 9 | from .development import Development, ThunderbirdDevelopment 10 | from .review_app import ReviewApp, ThunderbirdReviewApp 11 | from .staging import Staging, ThunderbirdStaging 12 | from .production import Production, ThunderbirdProduction 13 | 14 | # Exported Django Configuration objects 15 | __all__ = [ 16 | 'env', 17 | 'Base', 18 | 'Braintree', 19 | 'Database', 20 | 'Redis', 21 | 'Secure', 22 | 'Sentry', 23 | 'Testing', 24 | 'Development', 25 | 'Staging', 26 | 'ReviewApp', 27 | 'Production', 28 | 'ThunderbirdDevelopment', 29 | 'ThunderbirdStaging', 30 | 'ThunderbirdReviewApp', 31 | 'ThunderbirdProduction', 32 | ] 33 | -------------------------------------------------------------------------------- /source/sass/components/_heading.scss: -------------------------------------------------------------------------------- 1 | .heading { 2 | @include heading-text(); 3 | margin-bottom: 0; 4 | margin-top: 0; 5 | 6 | &--primary { 7 | @include font-size(xl); 8 | line-height: 1.1; 9 | font-weight: $weight--light; 10 | @include media-query(tablet-portrait) { 11 | @include font-size(xxl); 12 | } 13 | 14 | .app--404 &, 15 | .app--403 & { 16 | font-size: 48px; // Custom for 404 17 | font-weight: $weight--medium; 18 | color: $color--black; 19 | margin-bottom: $gutter; 20 | } 21 | 22 | .app--403 & { 23 | margin-bottom: $gutter * 4; 24 | } 25 | } 26 | 27 | &--secondary { 28 | @include font-size(l); 29 | font-weight: $weight--bold; 30 | } 31 | 32 | &--tertiary { 33 | @include font-size(m); 34 | } 35 | 36 | &--bottom-margin { 37 | margin-bottom: ($gutter * 1.5); 38 | } 39 | 40 | &--dark-bg { 41 | color: $color--white; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /donate/templates/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Server error 7 | 8 | 25 | 26 | 27 |

Something went wrong

28 |

We’re sorry! Our server is acting up right now. Please try again soon.

29 |

Internal Server Error (500)

30 | 31 | 32 | -------------------------------------------------------------------------------- /donate/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "pages/base_page.html" %} 2 | {% load i18n wagtailcore_tags wagtailsettings_tags static %} 3 | 4 | {% block title %}{% trans "Page not found" %}{% endblock %} 5 | 6 | {% block template_name %}app--404{% endblock %} 7 | 8 | {% block content %} 9 |
10 |
11 |
12 |

{% trans "Uh oh!" %}

13 |

{% trans "Sorry, the page does not exist" %}

14 | {% trans "Go to our home page" %} 15 |
16 |
17 | Sad anthropomorphized 404 18 |
19 |
20 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /source/sass/components/_loading.scss: -------------------------------------------------------------------------------- 1 | @keyframes pulse { 2 | 0% { 3 | opacity: 0; 4 | } 5 | 25% { 6 | opacity: 1; 7 | transform: scale(2); 8 | } 9 | 75% { 10 | opacity: 0; 11 | transform: scale(1); 12 | background: $color--light-blue; 13 | } 14 | } 15 | 16 | .loading { 17 | margin: 1rem 0 4rem; 18 | display: flex; 19 | align-items: center; 20 | justify-content: flex-start; 21 | 22 | & > div { 23 | display: inline-block; 24 | width: 0.75rem; 25 | height: 0.75rem; 26 | border-radius: 100%; 27 | background: $color--blue; 28 | margin: 0 0.5rem; 29 | opacity: 0; 30 | transition: all 0.5s linear; 31 | transform-origin: center center; 32 | animation: pulse 3s 0s linear infinite both; 33 | 34 | &:nth-child(1) { 35 | margin-left: 0; 36 | } 37 | 38 | &:nth-child(2) { 39 | animation-delay: 0.5s; 40 | } 41 | 42 | &:nth-child(3) { 43 | animation-delay: 1s; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /source/js/components/menu-toggle.js: -------------------------------------------------------------------------------- 1 | class MenuToggle { 2 | static selector() { 3 | return "[data-menu-toggle]"; 4 | } 5 | 6 | constructor(node, openCb = () => {}, closeCb = () => {}) { 7 | this.node = node; 8 | 9 | // Any callbacks to be called on open or close. 10 | this.openCb = openCb; 11 | this.closeCb = closeCb; 12 | 13 | this.state = { 14 | open: false, 15 | }; 16 | 17 | this.bindEventListeners(); 18 | } 19 | 20 | bindEventListeners() { 21 | this.node.addEventListener("click", () => { 22 | this.toggle(); 23 | }); 24 | } 25 | 26 | toggle() { 27 | this.state.open ? this.close() : this.open(); 28 | } 29 | 30 | open() { 31 | this.node.classList.add("is-open"); 32 | this.openCb(); 33 | 34 | this.state.open = true; 35 | } 36 | 37 | close() { 38 | this.node.classList.remove("is-open"); 39 | this.closeCb(); 40 | 41 | this.state.open = false; 42 | } 43 | } 44 | 45 | export default MenuToggle; 46 | -------------------------------------------------------------------------------- /donate/utility/middleware.py: -------------------------------------------------------------------------------- 1 | from django.http.response import HttpResponseRedirectBase 2 | from django.conf import settings 3 | 4 | hostnames = settings.TARGET_DOMAINS 5 | 6 | 7 | class HttpResponseTemporaryRedirect(HttpResponseRedirectBase): 8 | status_code = 307 9 | 10 | 11 | class TargetDomainRedirectMiddleware: 12 | def __init__(self, get_response): 13 | self.get_response = get_response 14 | 15 | def __call__(self, request): 16 | request_host = request.META['HTTP_HOST'] 17 | protocol = 'https' if request.is_secure() else 'http' 18 | 19 | if request_host in hostnames: 20 | return self.get_response(request) 21 | 22 | # Redirect to the primary domain (hostnames[0]) 23 | redirect_url = '{protocol}://{hostname}{path}'.format( 24 | protocol=protocol, 25 | hostname=hostnames[0], 26 | path=request.get_full_path() 27 | ) 28 | 29 | return HttpResponseTemporaryRedirect(redirect_url) 30 | -------------------------------------------------------------------------------- /donate/payments/stripe_webhooks.py: -------------------------------------------------------------------------------- 1 | import json 2 | from django.http import HttpResponse, HttpResponseBadRequest 3 | from django.utils.decorators import method_decorator 4 | from django.views.decorators.csrf import csrf_exempt 5 | from django.views.generic import View 6 | 7 | from .tasks import queue, process_stripe_webhook 8 | 9 | 10 | @method_decorator(csrf_exempt, name='dispatch') 11 | class StripeWebhookView(View): 12 | http_method_names = ['post'] 13 | 14 | def post(self, request): 15 | signature = request.META['HTTP_STRIPE_SIGNATURE'] 16 | 17 | if not signature: 18 | return HttpResponseBadRequest(reason='HTTP_STRIPE_SIGNATURE must be provided') 19 | 20 | try: 21 | payload = json.loads(request.body) 22 | except json.JSONDecodeError: 23 | return HttpResponseBadRequest(reason='Payload is not valid JSON') 24 | 25 | queue.enqueue(process_stripe_webhook, payload, signature=signature) 26 | 27 | return HttpResponse() 28 | -------------------------------------------------------------------------------- /donate/templates/forms/form_field.html: -------------------------------------------------------------------------------- 1 | {% load form_tags i18n %} 2 | 3 | {% with widget_type=field|widget_type field_type=field|field_type %} 4 | 5 |
12 | 13 | {% if not widget_type == "checkbox_input" %} 14 | {% include "forms/_label.html" %} 15 | {% endif %} 16 | 17 | {% include "forms/_field.html" %} 18 | 19 | {% if front_end_validated %} 20 | 23 | {% else %} 24 | {% if field.errors %} 25 |
26 | {{ field.errors }} 27 |
28 | {% endif %} 29 | {% endif %} 30 | 31 |
32 | 33 | {% endwith %} 34 | -------------------------------------------------------------------------------- /donate/templates/fragments/language_switcher.html: -------------------------------------------------------------------------------- 1 | {% load i18n util_tags %} 2 | 3 |
{% csrf_token %} 4 | 5 | 22 |
23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /source/sass/components/_accordion-item.scss: -------------------------------------------------------------------------------- 1 | .accordion-item { 2 | $root: &; 3 | padding: ($gutter / 2) 0; 4 | border-bottom: 1px solid $color--border; 5 | 6 | &__content { 7 | @include font-size(s); 8 | display: none; 9 | padding: ($gutter * 1.5) 0 $gutter; 10 | } 11 | 12 | &__link { 13 | display: flex; 14 | flex-direction: row; 15 | justify-content: space-between; 16 | align-items: center; 17 | color: $color--black; 18 | outline: 0; 19 | } 20 | 21 | &__heading { 22 | @include font-size(l); 23 | margin: 0; 24 | } 25 | 26 | #{$root}__icon { 27 | margin-left: $gutter; 28 | 29 | &--add { 30 | display: block; 31 | } 32 | 33 | &--remove { 34 | display: none; 35 | } 36 | } 37 | 38 | &.is-open { 39 | #{$root}__content { 40 | display: block; 41 | } 42 | 43 | #{$root}__icon { 44 | &--add { 45 | display: none; 46 | } 47 | 48 | &--remove { 49 | display: block; 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/design-to-dev-handoff.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Design to Dev Handoff 3 | about: Issue template that comes with a design to dev handoff checklist 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 13 | 14 | Enter actual ticket description here 15 | 16 | --- 17 | 18 | **🗒 Design -> Dev Handoff Checklist** 19 | (Feel free to remove items that are not applicable.) 20 | 21 | - [ ] Design has been finalized 22 | - [ ] Link to original design ticket 23 | - [ ] Link to related ticket 24 | - [ ] Link to Redpen 25 | - [ ] Design changes been merged into the master Sketch file 26 | - [ ] Desktop and mobile mockups are included 27 | - [ ] Assets are included 28 | - [ ] Hover state for elements are designed 29 | - [ ] Other states for elements are designed (e.g., error state for form) 30 | -------------------------------------------------------------------------------- /donate/core/migrations/0002_featureflags.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.14 on 2023-01-18 04:21 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('core', '0001_squashed_0015_remove_old_wagtail_localize'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='FeatureFlags', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('enable_upsell_view', models.BooleanField(default=False, help_text='Checking this will allow the upsell view to be displayed after one-time donations', verbose_name='Enable the upsell view')), 19 | ('site', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, to='wagtailcore.site')), 20 | ], 21 | options={ 22 | 'abstract': False, 23 | }, 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /source/sass/components/_payments.scss: -------------------------------------------------------------------------------- 1 | .payments { 2 | &__braintree-error { 3 | margin: $gutter 0 ($gutter / 2); 4 | padding: ($gutter / 2) $gutter; 5 | background-color: $color--light-red; 6 | color: $color--black; 7 | text-align: center; 8 | } 9 | &__button { 10 | height: 45px; 11 | overflow: hidden; 12 | 13 | @include media-query(tablet-landscape) { 14 | height: 35px; 15 | } 16 | 17 | @include media-query(paypal-specific) { 18 | height: 45px; 19 | } 20 | 21 | &--paypal { 22 | position: relative; 23 | border-radius: 4px; 24 | .paypal-disabled & { 25 | display: none; 26 | } 27 | &--overlay { 28 | display: none; 29 | position: absolute; 30 | @include z-index(overlay); 31 | top: 0; 32 | right: 0; 33 | bottom: 0; 34 | left: 0; 35 | cursor: pointer; 36 | } 37 | } 38 | } 39 | } 40 | 41 | // Vendor-generated classes 42 | .braintree-hosted-fields-invalid { 43 | border: 1px solid $color--red; 44 | } 45 | -------------------------------------------------------------------------------- /donate/thunderbird/templates/base.html: -------------------------------------------------------------------------------- 1 | {% extends "base_master.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block ga_identifier %} 6 | 7 | {% endblock %} 8 | 9 | {% block datalayer_initial_script %} 10 | {% endblock %} 11 | 12 | {% block fundraiseup_script %} 13 | 14 | 21 | 22 | {% endblock %} 23 | 24 | {% block title_suffix %} | {% trans 'Give to Thunderbird' context 'Page title' %}{% endblock %} 25 | -------------------------------------------------------------------------------- /donate/payments/tests/test_braintree_webhooks.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from django.test import TestCase 4 | 5 | from ..braintree_webhooks import WebhookForm, BraintreeWebhookView 6 | from ..tasks import process_webhook 7 | 8 | 9 | class BraintreeWebhookViewTestCase(TestCase): 10 | 11 | def test_form_valid_queues_task(self): 12 | form = WebhookForm({ 13 | 'bt_signature': 'signature', 14 | 'bt_payload': 'payload' 15 | }) 16 | assert form.is_valid() 17 | with mock.patch('donate.payments.braintree_webhooks.queue') as mock_queue: 18 | response = BraintreeWebhookView().form_valid(form) 19 | 20 | mock_queue.enqueue.assert_called_once_with( 21 | process_webhook, form.cleaned_data, 22 | description="Handle Braintree webhook" 23 | ) 24 | self.assertEqual(response.status_code, 200) 25 | 26 | def test_form_invalid_returns_400(self): 27 | form = WebhookForm({}) 28 | response = BraintreeWebhookView().form_invalid(form) 29 | self.assertEqual(response.status_code, 400) 30 | -------------------------------------------------------------------------------- /source/sass/pages/ways-to-give-page.scss: -------------------------------------------------------------------------------- 1 | .app--ways-to-give { 2 | .banner { 3 | background: $color--black; 4 | color: $color--white; 5 | height: 300px; 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: center; 9 | align-items: center; 10 | 11 | h1 { 12 | @include font-size(xxxl); 13 | margin: 0; 14 | } 15 | 16 | .button { 17 | display: inline-block; 18 | margin: 1rem 0; 19 | } 20 | } 21 | 22 | .container { 23 | padding: 2em 10px; 24 | max-width: 960px; 25 | width: 100%; 26 | margin: 0 auto; 27 | } 28 | 29 | .content { 30 | h2 { 31 | @include font-size(xxl); 32 | } 33 | 34 | h3 { 35 | @include font-size(xl); 36 | } 37 | 38 | .currency-list { 39 | list-style: initial; 40 | padding-left: 1rem; 41 | column-count: 2; 42 | 43 | @include media-query(mob-landscape) { 44 | column-count: 3; 45 | } 46 | 47 | @include media-query(tablet-landscape) { 48 | column-count: 4; 49 | } 50 | } 51 | } 52 | 53 | address { 54 | margin: 0 0 $gutter; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /donate/thunderbird/templates/payment/card.html: -------------------------------------------------------------------------------- 1 | {% extends "payment/card_master.html" %} 2 | {% load i18n %} 3 | 4 | 5 | {% block donate_footer %} 6 |

{% trans "Thank you in advance for your gift." %}

7 | {% endblock %} 8 | 9 | {% block donateFormAuth %} 10 | 17 | {% endblock%} 18 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | queue_rules: 2 | - name: dependabot 3 | conditions: 4 | - "status-success=continuous-integration/travis-ci/pr" 5 | - "status-success=continuous-integration/travis-ci/push" 6 | 7 | - name: ready-to-merge 8 | conditions: 9 | - "status-success=continuous-integration/travis-ci/pr" 10 | - "status-success=continuous-integration/travis-ci/push" 11 | 12 | pull_request_rules: 13 | - name: Automatic merge for dependabot PRs 14 | conditions: 15 | - "#approved-reviews-by>=1" 16 | - author~=^dependabot(|-preview)\[bot\]$ 17 | - "status-success=continuous-integration/travis-ci/pr" 18 | - "status-success=continuous-integration/travis-ci/push" 19 | actions: 20 | queue: 21 | method: squash 22 | name: dependabot 23 | 24 | - name: Automatic merge for PRs labelled "ready to merge" 25 | conditions: 26 | - "#approved-reviews-by>=1" 27 | - "status-success=continuous-integration/travis-ci/pr" 28 | - "status-success=continuous-integration/travis-ci/push" 29 | - "label=ready-to-merge" 30 | actions: 31 | queue: 32 | method: squash 33 | name: ready-to-merge 34 | -------------------------------------------------------------------------------- /translation-management.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # Pretty printing functions 6 | NORMAL=$(tput sgr0) 7 | GREEN=$(tput setaf 2; tput bold) 8 | YELLOW=$(tput setaf 3) 9 | RED=$(tput setaf 1) 10 | 11 | function echored() { 12 | echo -e "$RED$*$NORMAL" 13 | } 14 | 15 | function echogreen() { 16 | echo -e "$GREEN$*$NORMAL" 17 | } 18 | 19 | function echoyellow() { 20 | echo -e "$YELLOW$*$NORMAL" 21 | } 22 | 23 | if [[ $# -lt 1 ]] 24 | then 25 | echored "ERROR: not enough arguments supplied." 26 | echo "Usage: translation-management.sh *action*" 27 | echo " - action: import, export" 28 | exit 1 29 | fi 30 | 31 | command="$1" 32 | 33 | # Read path to local string repository from .env file 34 | L10N_REPO=$(grep LOCAL_PATH_TO_L10N_REPO .env | cut -d '=' -f2) 35 | 36 | case $command in 37 | "import") 38 | echogreen "Importing latest translation files from fomo-l10n repository" 39 | cp -r "${L10N_REPO}donate/locale/" "donate/locale/" 40 | esac 41 | 42 | case $command in 43 | "export") 44 | echogreen "Exporting generated translation files to fomo-l10n repository" 45 | cp -r "donate/locale/" "${L10N_REPO}donate/locale/" 46 | esac 47 | -------------------------------------------------------------------------------- /donate/settings/s3.py: -------------------------------------------------------------------------------- 1 | from .environment import env, root 2 | 3 | 4 | class S3(object): 5 | # S3 credentials for SQS and S3 6 | USE_S3 = env('USE_S3') 7 | AWS_ACCESS_KEY_ID = env('AWS_ACCESS_KEY_ID') 8 | AWS_SECRET_ACCESS_KEY = env('AWS_SECRET_ACCESS_KEY') 9 | AWS_LOCATION = env('AWS_LOCATION') 10 | AWS_REGION = env('AWS_REGION') 11 | 12 | AWS_STORAGE_BUCKET_NAME = env('AWS_STORAGE_BUCKET_NAME') 13 | AWS_S3_CUSTOM_DOMAIN = env('AWS_S3_CUSTOM_DOMAIN') 14 | 15 | @property 16 | def DEFAULT_FILE_STORAGE(self): 17 | if self.USE_S3: 18 | return 'storages.backends.s3boto3.S3Boto3Storage' 19 | 20 | # Use Django default 21 | return 'django.core.files.storage.FileSystemStorage' 22 | 23 | @property 24 | def MEDIA_URL(self): 25 | if self.USE_S3: 26 | return 'https://' + self.AWS_S3_CUSTOM_DOMAIN + '/' 27 | 28 | return '/media/' 29 | 30 | @property 31 | def MEDIA_ROOT(self): 32 | if self.USE_S3: 33 | return '' 34 | 35 | return root('media/') 36 | 37 | # This is a workaround for https://github.com/wagtail/wagtail/issues/3206 38 | AWS_S3_FILE_OVERWRITE = False 39 | -------------------------------------------------------------------------------- /donate/recaptcha/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from django.test import TestCase 4 | 5 | from requests.exceptions import RequestException 6 | 7 | from ..utils import verify 8 | 9 | 10 | class RecaptchaVerifyTestCase(TestCase): 11 | 12 | def test_verify_successful(self): 13 | with mock.patch('donate.recaptcha.utils.requests', autospec=True) as mock_requests: 14 | mock_requests.post.return_value.json.return_value = { 15 | 'success': True 16 | } 17 | self.assertTrue(verify('a-token', 'a-secret')) 18 | 19 | def test_verify_unsuccessful(self): 20 | with mock.patch('donate.recaptcha.utils.requests', autospec=True) as mock_requests: 21 | mock_requests.post.return_value.json.return_value = { 22 | 'success': False 23 | } 24 | self.assertFalse(verify('a-token', 'a-secret')) 25 | 26 | def test_verify_returns_true_if_request_failed(self): 27 | with mock.patch('donate.recaptcha.utils.requests', autospec=True) as mock_requests: 28 | mock_requests.post.side_effect = RequestException() 29 | self.assertTrue(verify('a-token', 'a-secret')) 30 | -------------------------------------------------------------------------------- /donate/core/blocks.py: -------------------------------------------------------------------------------- 1 | from wagtail.core import blocks 2 | from wagtail.images.blocks import ImageChooserBlock 3 | 4 | 5 | class HeadingBlock(blocks.CharBlock): 6 | 7 | class Meta: 8 | form_classname = 'full title' 9 | icon = 'title' 10 | template = 'blocks/heading_block.html' 11 | 12 | 13 | class ImageBlock(blocks.StructBlock): 14 | image = ImageChooserBlock() 15 | caption = blocks.CharBlock(required=False) 16 | 17 | class Meta: 18 | icon = 'image' 19 | template = 'blocks/image_block.html' 20 | 21 | 22 | class AccordionItem(blocks.StructBlock): 23 | title = blocks.CharBlock() 24 | content = blocks.RichTextBlock(features=['bold', 'italic', 'ol', 'ul', 'link']) 25 | 26 | 27 | class AccordionBlock(blocks.StructBlock): 28 | title = blocks.CharBlock() 29 | items = blocks.StreamBlock([ 30 | ('item', AccordionItem()), 31 | ]) 32 | 33 | class Meta: 34 | template = 'blocks/accordion_block.html' 35 | 36 | 37 | class ContentBlock(blocks.StreamBlock): 38 | heading = HeadingBlock() 39 | paragraph = blocks.RichTextBlock(features=['bold', 'italic', 'ol', 'ul', 'link']) 40 | image = ImageBlock() 41 | accordion = AccordionBlock() 42 | -------------------------------------------------------------------------------- /donate/thunderbird/templates/pages/core/contributor_support_page.html: -------------------------------------------------------------------------------- 1 | {% extends "pages/core/contributor_support_page_master.html" %} 2 | {% load i18n %} 3 | 4 | {% block donate_support_text %} 5 |

6 | {% blocktrans trimmed %} 7 | If you need help with a contribution to MZLA/Thunderbird, please fill out this form and a Thunderbird donor care representative will get back to you as soon as possible. 8 | {% endblocktrans %} 9 |

10 | 11 |

12 | {% blocktrans with support_url="https://support.mozilla.org/products/thunderbird/" trimmed %} 13 | Unfortunately, donor care representatives are unable to offer support or help with Thunderbird technical issues. For technical support, please visit our support page. 14 | {% endblocktrans %} 15 |

16 | {% endblock %} 17 | 18 | {% block captcha_settings %} 19 | 20 | {% endblock %} 21 | 22 | {% block custom_hidden_fields %} 23 | 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /donate/templates/admin/login.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/login.html" %} 2 | {% load auth_tags %} 3 | 4 | {% block content %} 5 | 6 | {% use_conventional_auth as conventional_auth_enabled %} 7 | 8 | {% if conventional_auth_enabled %} 9 | 10 | {{block.super}} 11 | 12 | {% else %} 13 | 14 | {% if form.errors and not form.non_field_errors %} 15 |

16 | {% if form.errors.items|length == 1 %}Please correct the error below.{% else %}Please correct the errors below.{% endif %} 17 |

18 | {% endif %} 19 | 20 | {% if form.non_field_errors %} 21 | {% for error in form.non_field_errors %} 22 |

23 | {{ error }} 24 |

25 | {% endfor %} 26 | {% endif %} 27 | 28 |

29 | 30 | Sign in with Mozilla SSO 31 | 32 |

33 |

34 | Note that after sign-in, you will be sent back to the CMS admin. Please re-access {% url 'admin:index' %} manually. 35 |

36 |

37 | 38 | If you lack SSO access, please ask your contact within Mozilla. 39 | 40 |

41 | {% endif %} 42 | {% endblock content %} 43 | -------------------------------------------------------------------------------- /source/images/mozilla-logo-black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /source/images/mozilla-logo-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /maintenance/static/_images/mozilla-logo-black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /maintenance/static/_images/mozilla-logo-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /source/sass/components/_footer-links.scss: -------------------------------------------------------------------------------- 1 | .footer-links { 2 | &__container { 3 | display: flex; 4 | flex-direction: column; 5 | @include media-query(tablet-landscape) { 6 | flex-direction: row; 7 | } 8 | } 9 | &__header { 10 | flex-basis: 100%; 11 | max-width: 100%; 12 | min-width: 100%; 13 | margin-bottom: ($gutter * 2); 14 | 15 | @include media-query(tablet-landscape) { 16 | flex-basis: 33.333%; 17 | max-width: 33.333%; 18 | min-width: 33.333%; 19 | margin-bottom: $gutter; 20 | margin-right: $gutter; 21 | } 22 | } 23 | &__group { 24 | margin-bottom: ($gutter * 2); 25 | @include media-query(tablet-landscape) { 26 | column-count: 2; 27 | } 28 | } 29 | 30 | &__item { 31 | @include body-text(); 32 | 33 | &:not(:last-child) { 34 | margin-bottom: 0.5rem; 35 | } 36 | 37 | &:last-child { 38 | margin-bottom: 0; 39 | } 40 | 41 | @include media-query(tablet-landscape) { 42 | break-inside: avoid; 43 | } 44 | } 45 | 46 | &__social-icon { 47 | width: 18px; 48 | height: 18px; 49 | margin-right: ($gutter / 2); 50 | transition: fill $transition; 51 | 52 | &:hover { 53 | fill: $color--light-blue; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /source/sass/components/_primary-nav.scss: -------------------------------------------------------------------------------- 1 | .primary-nav { 2 | $root: &; 3 | position: relative; 4 | 5 | @include media-query(tablet-landscape) { 6 | max-width: $site-width--large; 7 | margin: 0 auto; 8 | } 9 | 10 | &__container { 11 | @include z-index(nav); 12 | pointer-events: none; 13 | position: absolute; 14 | top: 0; 15 | width: 100%; 16 | padding-top: 100px; 17 | padding-bottom: 100px; 18 | display: flex; 19 | align-items: center; 20 | flex-direction: column; 21 | background-color: $color--black; 22 | transform: translateY(-8px); 23 | opacity: 0; 24 | transition: opacity 0.15s, transform 0.15s, visibility 0s linear 0.15s; 25 | 26 | @include media-query(tablet-landscape) { 27 | @include z-index(nav-desktop); 28 | top: -50px; 29 | left: ($gutter * 11.5); 30 | flex-direction: row; 31 | padding-top: 0; 32 | padding-bottom: 0; 33 | transform: translateY(8px); 34 | background-color: transparent; 35 | } 36 | 37 | @include media-query(desktop) { 38 | left: ($gutter * 10.5); 39 | } 40 | } 41 | 42 | &.is-visible { 43 | #{$root}__container { 44 | pointer-events: auto; 45 | transform: translateY(0); 46 | opacity: 1; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /donate/thunderbird/templates/pages/core/ways_to_give_page.html: -------------------------------------------------------------------------------- 1 | {% extends "pages/core/ways_to_give_page_master.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block stock_donations %}{% endblock %} 6 | 7 | {% block covid_notice %} 8 | {% blocktrans trimmed %} 9 | MZLA/Thunderbird is happy to accept your donation (made payable to “MZLA Technologies Corp”) via check; however please note that processing and acknowledgment of your gift may be delayed by changes to our office procedures due to the COVID-19 pandemic. You can send checks to: 10 | {% endblocktrans %} 11 | {% endblock %} 12 | 13 | {% block check_address %} 14 | MZLA Technologies Corporation
15 | Attn: Thunderbird Donor Care
16 | 2 Harrison Street, Suite 175
17 | San Francisco, CA 94105 18 | {% endblock %} 19 | 20 | {% block covid_notice_memo_line %} 21 | {% trans "Please include your email address on the memo line of your check to help us track and attribute your gift." %} 22 | {% endblock %} 23 | 24 | {% block covid_notice_end_note %} 25 | {% trans "Please also note that MZLA/Thunderbird can only accept checks in U.S. dollars drawn on a U.S. bank." %} 26 | {% endblock %} 27 | 28 | {% block currencies %} 29 | {% endblock %} 30 | 31 | {% block cryptocurrencies %}{% endblock %} 32 | -------------------------------------------------------------------------------- /donate/templates/403.html: -------------------------------------------------------------------------------- 1 | {% extends "pages/base_page.html" %} 2 | {% load i18n wagtailcore_tags wagtailsettings_tags static %} 3 | 4 | {% block title %}{% trans "VPN Issue" %}{% endblock %} 5 | 6 | {% block template_name %}app--403{% endblock %} 7 | 8 | {% block content %} 9 |
10 |
11 |

{% trans "Action Required - VPN Issue" %}

12 |
13 |
14 |
15 |

16 | {% blocktrans with url="/" site=request.get_host trimmed %} 17 | Please disable your VPN and visit {{ site }} again. 18 | {% endblocktrans %} 19 |

20 |

21 | {% trans "We take your privacy very seriously. To minimize fraudulent payments, we’re asking supporters to disable their VPNs when visiting our donate page." %} 22 |

23 |
24 |
25 | Sad anthropomorphized 403 26 |
27 |
28 |
29 | {% endblock %} -------------------------------------------------------------------------------- /source/js/components/tabs.js: -------------------------------------------------------------------------------- 1 | import gaEvent from "./analytics"; 2 | 3 | class Tabs { 4 | static selector() { 5 | return ".js-tab-item"; 6 | } 7 | 8 | constructor(node) { 9 | this.tab = node; 10 | this.tabset = this.tab.closest(".js-tabs"); 11 | this.allTabs = this.tabset.querySelectorAll(".js-tab-item"); 12 | let tabPanelId = this.tab.getAttribute("aria-controls"); 13 | this.tabPanel = document.getElementById(tabPanelId); 14 | this.allTabPanels = this.tabset.querySelectorAll(".js-tab-panel"); 15 | this.bindEvents(); 16 | } 17 | 18 | bindEvents() { 19 | this.tab.addEventListener("click", (e) => { 20 | for (let tab of this.allTabs) { 21 | tab.classList.remove("active"); 22 | tab.setAttribute("aria-selected", "false"); 23 | } 24 | 25 | for (let tabPanel of this.allTabPanels) { 26 | tabPanel.classList.add("tabs__panel--hidden"); 27 | } 28 | 29 | this.tab.classList.add("active"); 30 | this.tab.setAttribute("aria-selected", "true"); 31 | this.tabPanel.classList.remove("tabs__panel--hidden"); 32 | gaEvent({ 33 | eventCategory: "User Flow", 34 | eventAction: "Changed Frequency", 35 | eventLabel: this.tab.getAttribute("data-label"), 36 | }); 37 | }); 38 | } 39 | } 40 | 41 | export default Tabs; 42 | -------------------------------------------------------------------------------- /source/images/favicon/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /maintenance/static/_images/favicon/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /donate/templates/tags/primarynav.html: -------------------------------------------------------------------------------- 1 | {% load wagtailcore_tags i18n %} 2 | 24 | -------------------------------------------------------------------------------- /source/sass/components/_burger.scss: -------------------------------------------------------------------------------- 1 | .burger { 2 | $root: &; 3 | cursor: pointer; 4 | border: 0; 5 | background: transparent; 6 | width: 28px; 7 | height: 28px; 8 | padding: 0; 9 | position: relative; 10 | @include z-index(base); 11 | outline: 0; 12 | transition: background-color $transition; 13 | 14 | &:focus, 15 | &:hover { 16 | #{$root}__bar { 17 | background-color: $color--grey-60; 18 | } 19 | } 20 | 21 | &__bar { 22 | position: absolute; 23 | background-color: $color--black; 24 | height: 3px; 25 | width: 28px; 26 | transition: top 0.1s linear 0.1s, width 0.1s linear 0.1s, transform 0.1s; 27 | 28 | &--top { 29 | top: 3px; 30 | width: 16px; 31 | } 32 | 33 | &--middle { 34 | top: 12.5px; 35 | } 36 | 37 | &--bottom { 38 | top: 22px; 39 | width: 23px; 40 | } 41 | } 42 | 43 | &.is-open { 44 | #{$root}__bar { 45 | transition: top 0.1s, width 0.1s, transform 0.1s linear 0.1s; 46 | 47 | &--top { 48 | top: 12.5px; 49 | width: 28px; 50 | transform: rotate(135deg); 51 | } 52 | 53 | &--middle { 54 | transform: rotate(135deg); 55 | } 56 | 57 | &--bottom { 58 | top: 12.5px; 59 | width: 28px; 60 | transform: rotate(45deg); 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /donate/settings/redis.py: -------------------------------------------------------------------------------- 1 | from .environment import env 2 | 3 | 4 | class Redis(object): 5 | REDIS_URL = env('REDIS_URL') 6 | REDIS_QUEUE_URL = env('REDIS_QUEUE_URL', default=REDIS_URL) 7 | 8 | connection_pool_kwargs = {} 9 | 10 | if REDIS_URL.startswith("rediss"): 11 | connection_pool_kwargs["ssl_cert_reqs"] = None 12 | 13 | if REDIS_QUEUE_URL.startswith('rediss'): 14 | REDIS_QUEUE_URL = REDIS_QUEUE_URL + "?ssl_cert_reqs=none" 15 | 16 | CACHES = { 17 | 'default': { 18 | 'BACKEND': 'django_redis.cache.RedisCache', 19 | 'LOCATION': REDIS_URL, 20 | 'OPTIONS': { 21 | 'SOCKET_TIMEOUT': 120, 22 | 'SOCKET_CONNECT_TIMEOUT': 30, 23 | 'COMPRESSOR': 'django_redis.compressors.zlib.ZlibCompressor', 24 | 'IGNORE_EXCEPTIONS': True, 25 | "CONNECTION_POOL_KWARGS": connection_pool_kwargs 26 | } 27 | } 28 | } 29 | 30 | RQ_QUEUES = { 31 | 'default': { 32 | 'URL': REDIS_QUEUE_URL, 33 | 'DEFAULT_TIMEOUT': 500, 34 | }, 35 | # Must be a separate queue as it's limited to one item at a time 36 | 'wagtail_localize_pontoon.sync': { 37 | 'URL': REDIS_QUEUE_URL, 38 | 'DEFAULT_TIMEOUT': 500, 39 | 'SSL_CERT_REQS': None 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/integration.spec.js: -------------------------------------------------------------------------------- 1 | const { test, expect } = require("@playwright/test"); 2 | const waitForImagesToLoad = require("./wait-for-images.js"); 3 | 4 | test(`Donate homepage`, async ({ page }, _testInfo) => { 5 | page.on(`console`, console.log); 6 | await page.goto(`http://localhost:8000/en-US/`); 7 | await waitForImagesToLoad(page); 8 | 9 | const logo = await page.locator(`a.header__logo-link`); 10 | expect(await logo.isVisible()).toBe(true); 11 | 12 | 13 | // The tests below have been commented out because we are no longer using our 14 | // HTML donate form. Instead, we are using a FundraiseUp form component that is loaded in through javascript. 15 | // See: https://github.com/MozillaFoundation/foundation.mozilla.org/issues/10261 16 | 17 | // // default view is single 18 | // const form = await page.locator(`.donate-form--single`); 19 | // expect(await form.isVisible()).toBe(true); 20 | 21 | // values match expected values - TODO: figure out how we can marry python constants with JS testing 22 | // const inputs = await page.locator( 23 | // `#donate-form--single .donation-amount input` 24 | // ); 25 | // const values = []; 26 | // const count = await inputs.count(); 27 | // for (let i = 0; i < count; ++i) { 28 | // values[i] = parseFloat(await inputs.nth(i).inputValue()); 29 | // } 30 | // expect(values).toEqual([10, 20, 30, 60, NaN, NaN]); 31 | }); 32 | -------------------------------------------------------------------------------- /source/js/components/accordion.js: -------------------------------------------------------------------------------- 1 | class Accordion { 2 | static selector() { 3 | return "[data-accordion]"; 4 | } 5 | 6 | constructor(node) { 7 | this.accordion = node; 8 | this.question = this.accordion.querySelector("[data-accordion-question]"); 9 | this.answer = this.accordion.querySelector("[data-accordion-answer]"); 10 | this.bindEvents(); 11 | } 12 | 13 | bindEvents() { 14 | this.question.addEventListener("click", (e) => { 15 | e.preventDefault(); 16 | let open = this.accordion.classList.contains("is-open"); 17 | this.accordion.classList.toggle("is-open"); 18 | 19 | if (open) { 20 | this.question.setAttribute("aria-expanded", "false"); 21 | this.question.setAttribute("tab-index", 0); 22 | this.answer.setAttribute("aria-hidden", "true"); 23 | open = false; 24 | } else { 25 | this.question.setAttribute("aria-expanded", "true"); 26 | this.question.setAttribute("tab-index", -1); 27 | this.answer.setAttribute("aria-hidden", "false"); 28 | open = true; 29 | } 30 | }); 31 | 32 | this.question.addEventListener("focus", () => { 33 | this.question.setAttribute("aria-selected", "true"); 34 | }); 35 | 36 | this.question.addEventListener("blur", () => { 37 | this.question.setAttribute("aria-selected", "false"); 38 | }); 39 | } 40 | } 41 | 42 | export default Accordion; 43 | -------------------------------------------------------------------------------- /source/sass/components/_donation-update.scss: -------------------------------------------------------------------------------- 1 | .donation-update { 2 | display: none; 3 | 4 | &.active { 5 | display: block; 6 | } 7 | 8 | &__toggle { 9 | color: $color--dark-blue; 10 | 11 | &.selected { 12 | color: $color--black; 13 | } 14 | } 15 | 16 | &__form-item { 17 | position: relative; 18 | } 19 | 20 | &__label { 21 | @include hidden(); 22 | } 23 | 24 | &__currency-symbol { 25 | position: absolute; 26 | left: 0; 27 | top: 0; 28 | height: 100%; 29 | pointer-events: none; 30 | border-right: 1px solid $color--border; 31 | padding: ($gutter * 0.8) ($gutter); 32 | text-align: center; 33 | @include media-query(tablet-portrait) { 34 | padding: ($gutter * 1.05) ($gutter); 35 | } 36 | } 37 | 38 | &__input { 39 | padding-left: 70px !important; // allow space for currency symbol 40 | height: 100%; 41 | } 42 | 43 | &__action { 44 | width: 100%; 45 | } 46 | 47 | &__container { 48 | display: block; 49 | width: 100%; 50 | @include media-query(tablet-portrait) { 51 | display: inline-flex; 52 | } 53 | } 54 | 55 | &__confirm-button { 56 | width: 100%; 57 | @include media-query(tablet-portrait) { 58 | width: 25%; 59 | } 60 | } 61 | &__amount-input { 62 | width: 100%; 63 | margin-right: 16px; 64 | @include media-query(tablet-portrait) { 65 | width: 75%; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /donate/payments/tests/test_forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.test import TestCase 3 | 4 | from .. import constants 5 | from ..forms import MinimumCurrencyAmountMixin 6 | 7 | 8 | class MinimumCurrencyTestForm(MinimumCurrencyAmountMixin, forms.Form): 9 | amount = forms.DecimalField() 10 | currency = forms.ChoiceField(choices=constants.CURRENCY_CHOICES, widget=forms.HiddenInput) 11 | frequency = forms.ChoiceField(choices=constants.FREQUENCY_CHOICES, widget=forms.HiddenInput) 12 | 13 | 14 | class MinimumCurrencyAmountMixinTestCase(TestCase): 15 | 16 | def test_init_sets_min_attr_if_currency_and_frequency_supplied(self): 17 | form = MinimumCurrencyTestForm(initial={'currency': 'usd', 'frequency': constants.FREQUENCY_SINGLE}) 18 | self.assertEqual(form.fields['amount'].widget.attrs['min'], 10) 19 | 20 | def test_clean_validates_minimum_amount_single(self): 21 | form = MinimumCurrencyTestForm({'amount': 1, 'currency': 'usd', 'frequency': constants.FREQUENCY_SINGLE}) 22 | self.assertFalse(form.is_valid()) 23 | self.assertEqual(form.errors, {'amount': ['Donations must be $10 or more']}) 24 | 25 | def test_clean_validates_minimum_amount_monthly(self): 26 | form = MinimumCurrencyTestForm({'amount': 1, 'currency': 'usd', 'frequency': constants.FREQUENCY_MONTHLY}) 27 | self.assertFalse(form.is_valid()) 28 | self.assertEqual(form.errors, {'amount': ['Donations must be $5 or more']}) 29 | -------------------------------------------------------------------------------- /donate/settings/testing.py: -------------------------------------------------------------------------------- 1 | from configurations import Configuration 2 | from .base import Base 3 | from .secure import Secure 4 | from .redis import Redis 5 | from .oidc import OIDC 6 | from .database import Database 7 | from .braintree import Braintree 8 | 9 | 10 | class Testing(Base, Secure, Redis, OIDC, Database, Braintree, Configuration): 11 | SECRET_KEY = 'test' 12 | USE_RECAPTCHA = False 13 | RECAPTCHA_SITE_KEY = 'test' 14 | RECAPTCHA_SECRET_KEY = 'test' 15 | RECAPTCHA_SITE_KEY_CHECKBOX = 'test' 16 | RECAPTCHA_SECRET_KEY_CHECKBOX = 'test' 17 | RECAPTCHA_SITE_KEY_REGULAR = 'test' 18 | 19 | BRAINTREE_MERCHANT_ID = 'test' 20 | BRAINTREE_PRIVATE_KEY = 'test' 21 | BRAINTREE_PUBLIC_KEY = 'test' 22 | BRAINTREE_USE_SANDBOX = True 23 | BRAINTREE_MERCHANT_ACCOUNTS = { 24 | 'usd': 'usd-ac', 25 | 'gbp': 'gbp-ac', 26 | } 27 | BRAINTREE_PLANS = { 28 | 'usd': 'usd-plan', 29 | 'gbp': 'gbp-plan', 30 | } 31 | 32 | BASKET_API_ROOT_URL = 'http://localhost' 33 | BASKET_SQS_QUEUE_URL = 'sqs.us-east-1.amazonaws.com/1234567890/test' 34 | AWS_REGION = 'us-east-1' 35 | AWS_STORAGE_BUCKET_NAME = 'test' 36 | 37 | HEROKU_APP_NAME = None 38 | 39 | @classmethod 40 | def pre_setup(cls): 41 | super().pre_setup() 42 | 43 | @classmethod 44 | def setup(cls): 45 | super().setup() 46 | 47 | @classmethod 48 | def post_setup(cls): 49 | super().post_setup() 50 | -------------------------------------------------------------------------------- /tests/wait-for-images.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Playwright has no built-in way to wait for all on-page images 3 | * to be flagged as completed, so we have our own function for that. 4 | * @param {Playwrite Page} page 5 | * @returns resolve() once all images are done, or reject() if after 20 tries images still haven't finished. 6 | */ 7 | module.exports = async function waitForImagesToLoad(page) { 8 | page.on(`console`, console.log); 9 | 10 | const images = page.locator("img"); 11 | 12 | return new Promise(async (resolve, reject) => { 13 | let cutoff = 20; 14 | (async function testLoaded() { 15 | // force-load all lazy content 16 | await images.evaluateAll((imgs) => { 17 | const visible = Array.from(imgs).filter((i) => i.offsetParent !== null); 18 | visible.forEach((img) => { 19 | if (img.loading === `lazy` && !img.complete) img.scrollIntoView(); 20 | }); 21 | }); 22 | 23 | // then check how many images aren't complete yet 24 | const result = await images.evaluateAll((imgs) => { 25 | const visible = Array.from(imgs).filter((i) => i.offsetParent !== null); 26 | return visible.filter((img) => !img.complete).length; 27 | }); 28 | 29 | if (result === 0) { 30 | return resolve(); 31 | } 32 | 33 | if (--cutoff === 0) { 34 | console.log(`Some images did not finished loading:`); 35 | return reject(); 36 | } 37 | 38 | setTimeout(testLoaded, 500); 39 | })(); 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /donate/settings/review_app.py: -------------------------------------------------------------------------------- 1 | from configurations import Configuration 2 | from .environment import env 3 | from .staging import Staging 4 | from .secure import Secure 5 | from .thunderbird import ThunderbirdOverrides 6 | 7 | 8 | class ReviewApp(Staging, Configuration): 9 | ALLOWED_HOSTS = [f'{Secure.HEROKU_APP_NAME}.herokuapp.com'] 10 | DEBUG = env('DEBUG') 11 | USE_RECAPTCHA = False 12 | HEROKU_PR_NUMBER = env('HEROKU_PR_NUMBER') 13 | HEROKU_BRANCH = env('HEROKU_BRANCH') 14 | 15 | @classmethod 16 | def pre_setup(cls): 17 | super().pre_setup() 18 | 19 | @classmethod 20 | def setup(cls): 21 | super().setup() 22 | 23 | @classmethod 24 | def post_setup(cls): 25 | super().post_setup() 26 | 27 | 28 | class ThunderbirdReviewApp(ReviewApp, ThunderbirdOverrides, Configuration): 29 | INSTALLED_APPS = ThunderbirdOverrides.INSTALLED_APPS + ReviewApp.INSTALLED_APPS 30 | FRONTEND = ThunderbirdOverrides.FRONTEND 31 | CURRENCIES = ThunderbirdOverrides.CURRENCIES 32 | FRAUD_SITE_ID = 'tbird' 33 | 34 | @property 35 | def TEMPLATES(self): 36 | config = ReviewApp.TEMPLATES 37 | config[0]['DIRS'] = ThunderbirdOverrides.TEMPLATES_DIR + config[0]['DIRS'] 38 | return config 39 | 40 | @classmethod 41 | def pre_setup(cls): 42 | super().pre_setup() 43 | 44 | @classmethod 45 | def setup(cls): 46 | super().setup() 47 | 48 | @classmethod 49 | def post_setup(cls): 50 | super().post_setup() 51 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | # Django runs twice to support live-reloading, so check Django's internal settings to determine whether or not 6 | # to start the debugger. 7 | if (os.environ.get("RUN_MAIN") or os.environ.get("WERKZEUG_RUN_MAIN")) and os.environ.get('VSCODE_DEBUGGER', False): 8 | import ptvsd # noqa 9 | ptvsd_port = 8001 10 | 11 | try: 12 | ptvsd.enable_attach(address=("0.0.0.0", ptvsd_port)) 13 | print(f"Started ptvsd server at port {ptvsd_port}") 14 | except OSError: 15 | print(f"ptvsd port {ptvsd_port} already in use") 16 | 17 | if __name__ == "__main__": 18 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "donate.settings") 19 | os.environ.setdefault("DJANGO_CONFIGURATION", 'Development') 20 | try: 21 | from configurations.management import execute_from_command_line 22 | except ImportError: 23 | # The above import may fail for some other reason. Ensure that the 24 | # issue is really that Django is missing to avoid masking other 25 | # exceptions on Python 2. 26 | try: 27 | import django # noqa: F401 28 | except ImportError: 29 | raise ImportError( 30 | "Couldn't import Django. Are you sure it's installed and " 31 | "available on your PYTHONPATH environment variable? Did you " 32 | "forget to activate a virtual environment?" 33 | ) 34 | raise 35 | execute_from_command_line(sys.argv) 36 | -------------------------------------------------------------------------------- /source/sass/components/_form.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --currency-width: 0; // populated with donation currency width js 3 | } 4 | 5 | .form { 6 | $root: &; 7 | 8 | &__container { 9 | margin-bottom: $gutter; 10 | 11 | input:not([type="checkbox"]):not([type="radio"]), 12 | select { 13 | min-height: 60px; 14 | } 15 | } 16 | 17 | &__errors { 18 | @include font-size(s); 19 | padding: $gutter; 20 | color: $color--black; 21 | background-color: $color--light-red; 22 | margin-bottom: $gutter; 23 | } 24 | 25 | &__group { 26 | &--two-col { 27 | display: grid; 28 | grid-template-columns: 1fr 1fr; 29 | } 30 | &--flex { 31 | display: flex; 32 | flex-direction: row; 33 | align-items: flex-end; 34 | } 35 | &--gutter { 36 | grid-gap: $gutter; 37 | } 38 | &--city-post { 39 | display: grid; 40 | 41 | // This gets updated to "Auto" in post-code-validation.js 42 | // If the form does not require a post code. 43 | grid-template-columns: 0.3fr 0.7fr; 44 | 45 | .form-item:nth-of-type(1) { 46 | input { 47 | border-right: 0; 48 | } 49 | } 50 | } 51 | 52 | &--additional-info { 53 | position: relative; 54 | 55 | input:not([type="checkbox"]) { 56 | .app--upsell & { 57 | padding-left: calc((var(--currency-width)) + ((#{$gutter}) * 1.5)); 58 | } 59 | } 60 | } 61 | } 62 | 63 | &__section { 64 | &--bottom-margin { 65 | margin-bottom: ($gutter * 1.5); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /donate/settings/secure.py: -------------------------------------------------------------------------------- 1 | from .environment import env 2 | 3 | 4 | class Secure(object): 5 | USE_X_FORWARDED_HOST = True 6 | CSRF_COOKIE_SECURE = True 7 | SESSION_COOKIE_SECURE = True 8 | SECURE_BROWSER_XSS_FILTER = True 9 | SECURE_CONTENT_TYPE_NOSNIFF = True 10 | SECURE_HSTS_INCLUDE_SUBDOMAINS = True 11 | SECURE_HSTS_SECONDS = 31556952 12 | SECURE_SSL_REDIRECT = True 13 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 14 | X_FRAME_OPTIONS = 'DENY' 15 | REFERRER_POLICY = 'no-referrer-when-downgrade' 16 | 17 | AUTH_PASSWORD_VALIDATORS = [ 18 | { 19 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 20 | }, 21 | { 22 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 23 | }, 24 | { 25 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 26 | }, 27 | { 28 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 29 | }, 30 | ] 31 | 32 | # Recaptcha 33 | USE_RECAPTCHA = True 34 | RECAPTCHA_SITE_KEY = env('RECAPTCHA_SITE_KEY') 35 | RECAPTCHA_SECRET_KEY = env('RECAPTCHA_SECRET_KEY') 36 | RECAPTCHA_SITE_KEY_CHECKBOX = env('RECAPTCHA_SITE_KEY_CHECKBOX') 37 | RECAPTCHA_SECRET_KEY_CHECKBOX = env('RECAPTCHA_SECRET_KEY_CHECKBOX') 38 | RECAPTCHA_SITE_KEY_REGULAR = env('RECAPTCHA_SITE_KEY_REGULAR') 39 | USE_CHECKBOX_RECAPTCHA_FOR_CC = env('USE_CHECKBOX_RECAPTCHA_FOR_CC') 40 | 41 | HEROKU_APP_NAME = env('HEROKU_APP_NAME') 42 | -------------------------------------------------------------------------------- /source/sass/components/_footer-custom-select.scss: -------------------------------------------------------------------------------- 1 | .footer-links { 2 | select { 3 | &.form-control { 4 | $background-size: 24px; 5 | $padding-x: 12px; 6 | $padding-right: $padding-x * 2 + $background-size; 7 | 8 | appearance: none; 9 | background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 8l8.05 8.05L20.1 8' stroke='%23FFF' stroke-width='2' fill='none' fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); 10 | background-color: transparent; 11 | background-repeat: no-repeat; 12 | background-position: right $padding-x top 50%; 13 | background-size: $background-size $background-size; 14 | border-radius: 0; 15 | border: 1px solid $color--white; 16 | padding: 5px $padding-right 5px $padding-x; 17 | color: $color--white; 18 | font-family: "Nunito Sans", sans-serif; 19 | 20 | // Explicitly setting