, and
22 | // we would end up having white text on white background in the dropdown.
23 | option {
24 | color: $color--black;
25 | }
26 | }
27 | }
28 | }
29 |
30 | .app--rtl {
31 | .footer-links {
32 | select {
33 | &.form-control {
34 | $background-size: 24px;
35 | $padding-x: 12px;
36 | $padding-left: $padding-x * 2 + $background-size;
37 | background-position: left $padding-x top 50%;
38 | padding: 5px $padding-x 5px $padding-left;
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/source/sass/base/_base.scss:
--------------------------------------------------------------------------------
1 | /*------------------------------------*\
2 | $BASE DEFAULTS
3 | \*------------------------------------*/
4 |
5 | // Box Sizing
6 | *,
7 | *::before,
8 | *::after {
9 | box-sizing: border-box;
10 | }
11 |
12 | // Prevent text size change on orientation change.
13 | // sass-lint:disable no-vendor-prefixes
14 | html {
15 | font-family: $font--primary;
16 | -webkit-text-size-adjust: 100%;
17 | -webkit-font-smoothing: antialiased;
18 | -moz-osx-font-smoothing: grayscale;
19 | background-color: $color--black; // match footer bg colour
20 | scroll-behavior: smooth;
21 |
22 | @media (prefers-reduced-motion: reduce) {
23 | scroll-behavior: auto;
24 | }
25 | }
26 | // sass-lint:enddisable
27 |
28 | body {
29 | overflow-x: hidden;
30 | background-color: $color--background;
31 |
32 | &.no-scroll {
33 | overflow-y: hidden;
34 | }
35 | }
36 |
37 | // Prevent empty space below images appearing
38 | img,
39 | svg {
40 | vertical-align: top;
41 | }
42 |
43 | // Responsive images
44 | img {
45 | height: auto;
46 | max-width: 100%;
47 | }
48 |
49 | // sass-lint:disable single-line-per-selector
50 | button,
51 | input:not([type="checkbox"]):not([type="radio"]),
52 | select,
53 | textarea {
54 | font-family: inherit;
55 | -webkit-appearance: none;
56 | border-radius: 0;
57 | }
58 |
59 | // sass-lint:enddisable
60 |
61 | a {
62 | color: $color--link;
63 | text-decoration: none;
64 | transition: color $transition;
65 |
66 | &:hover {
67 | color: $color--link-hover;
68 | cursor: pointer;
69 | }
70 | }
71 |
72 | ul,
73 | ol {
74 | padding: 0;
75 | margin: 0;
76 | list-style: none;
77 | }
78 |
--------------------------------------------------------------------------------
/source/sass/components/_newsletter-signup.scss:
--------------------------------------------------------------------------------
1 | .newsletter-signup {
2 | &__header {
3 | margin-bottom: ($gutter);
4 | }
5 | &__label {
6 | display: block;
7 | margin-bottom: ($gutter);
8 | }
9 | &__thanks {
10 | display: none; // toggled with js when email submitted
11 | }
12 | &__button {
13 | height: 50px;
14 | font-weight: $weight--bold;
15 | padding: ($gutter / 4) $gutter;
16 | margin-left: 0;
17 | min-width: 90px; // prevent word wrapping
18 | width: 100%;
19 |
20 | @include media-query(tablet-portrait) {
21 | margin-left: $gutter;
22 | width: auto;
23 | }
24 | }
25 | &__errors {
26 | color: $color--red;
27 | margin-bottom: ($gutter / 2);
28 | }
29 | &__privacy-label {
30 | @include font-size(xs);
31 | display: block; // To keep label from wrapping underneath checkbox
32 | margin-top: -23px; //
33 | margin-left: 20px; //
34 |
35 | a {
36 | color: $color--light-blue;
37 |
38 | &:hover {
39 | color: $color--blue;
40 | }
41 | }
42 | }
43 | &__form {
44 | .form-item {
45 | width: 100%;
46 |
47 | input:not([type="checkbox"]) {
48 | height: 49px;
49 | border: 2px solid $color--white;
50 | transition: border $transition;
51 | &:focus {
52 | border: 2px solid $color--light-blue;
53 | }
54 | }
55 | }
56 | .form__group--flex {
57 | flex-direction: column;
58 |
59 | @include media-query(tablet-portrait) {
60 | flex-direction: row;
61 | align-items: start;
62 | }
63 | }
64 | p {
65 | margin-bottom: $gutter;
66 | }
67 | }
68 | &__input {
69 | margin-bottom: $gutter;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/source/sass/main.scss:
--------------------------------------------------------------------------------
1 | // Vendor
2 | @import "vendor/normalize";
3 |
4 | // Abstracts
5 | @import "abstracts/variables";
6 | @import "abstracts/functions";
7 | @import "abstracts/mixins";
8 |
9 | // Base
10 | @import "base/base";
11 | @import "base/typography";
12 | @import "base/layout";
13 | @import "base/layout--fundraiseup";
14 | @import "base/utils";
15 |
16 | // Pages
17 | @import "pages/ways-to-give-page";
18 |
19 | // Components
20 | // Ordered alphabetically
21 | @import "components/accordion";
22 | @import "components/accordion-item";
23 | @import "components/app";
24 | @import "components/burger";
25 | @import "components/button";
26 | @import "components/card";
27 | @import "components/column-spacing";
28 | @import "components/donation-amount";
29 | @import "components/donate-form";
30 | @import "components/donation-update";
31 | @import "components/error";
32 | @import "components/footer";
33 | @import "components/footer-custom-select";
34 | @import "components/footer-links";
35 | @import "components/form";
36 | @import "components/form-custom-select";
37 | @import "components/form-item";
38 | @import "components/header";
39 | @import "components/heading";
40 | @import "components/hero";
41 | @import "components/icon";
42 | @import "components/image-feature";
43 | @import "components/introduction";
44 | @import "components/link";
45 | @import "components/logo-item";
46 | @import "components/logo-showcase";
47 | @import "components/nav-item";
48 | @import "components/newsletter-signup";
49 | @import "components/notification";
50 | @import "components/page-header";
51 | @import "components/payments";
52 | @import "components/primary-nav";
53 | @import "components/rich-text";
54 | @import "components/sticky-message";
55 | @import "components/support-form";
56 | @import "components/tabs";
57 | @import "components/loading";
58 |
--------------------------------------------------------------------------------
/donate/core/management/commands/load_fake_data.py:
--------------------------------------------------------------------------------
1 | import factory
2 | import random
3 |
4 | from django.core.management.base import BaseCommand
5 | from django.conf import settings
6 |
7 | import donate.core.factory as core_factory
8 | from donate.utility.faker.helpers import reseed
9 |
10 | from wagtail_factories import ImageFactory
11 |
12 |
13 | class Command(BaseCommand):
14 | help = 'Generate fake data for local development and testing purposes' \
15 | 'and load it into the database'
16 |
17 | def add_arguments(self, parser):
18 | parser.add_argument(
19 | '--seed',
20 | action='store',
21 | dest='seed',
22 | help='A seed value to pass to Faker before generating data',
23 | )
24 |
25 | def handle(self, *args, **options):
26 |
27 | faker = factory.faker.Faker._get_faker(locale='en-US')
28 |
29 | # Seed Faker with the provided seed value or a pseudorandom int between 0 and five million
30 | if options['seed']:
31 | seed = options['seed']
32 | elif settings.RANDOM_SEED is not None:
33 | seed = settings.RANDOM_SEED
34 | else:
35 | seed = random.randint(0, 5000000)
36 |
37 | self.stdout.write(f'Seeding random numbers with: {seed}')
38 |
39 | reseed(seed)
40 |
41 | self.stdout.write('Generating Images')
42 | for i in range(20):
43 | ImageFactory.create(
44 | file__width=1080,
45 | file__height=720,
46 | file__color=faker.safe_color_name()
47 | )
48 |
49 | factories = [
50 | core_factory,
51 | ]
52 | for app_factory in factories:
53 | app_factory.generate(seed)
54 |
55 | self.stdout.write(self.style.SUCCESS('Done!'))
56 |
--------------------------------------------------------------------------------
/donate/settings/staging.py:
--------------------------------------------------------------------------------
1 | from configurations import Configuration
2 | from .base import Base
3 | from .secure import Secure
4 | from .oidc import OIDC
5 | from .database import Database
6 | from .redis import Redis
7 | from .s3 import S3
8 | from .salesforce import Salesforce
9 | from .braintree import Braintree
10 | from .sentry import Sentry
11 | from .thunderbird import ThunderbirdOverrides
12 |
13 |
14 | class Staging(Base, Secure, OIDC, Database, Redis, S3, Salesforce, Braintree, Sentry, Configuration):
15 | DEBUG = False
16 | SALESFORCE_CASE_RECORD_TYPE_ID = "0124x000000A1ez"
17 |
18 | @classmethod
19 | def pre_setup(cls):
20 | super().pre_setup()
21 | cls.LOGGING['loggers']['rq.worker']['handlers'] = ['info']
22 | cls.LOGGING['loggers']['rq.worker']['level'] = 'INFO'
23 |
24 | @classmethod
25 | def setup(cls):
26 | super().setup()
27 |
28 | @classmethod
29 | def post_setup(cls):
30 | super().post_setup()
31 |
32 |
33 | class ThunderbirdStaging(Staging, ThunderbirdOverrides, Configuration):
34 | INSTALLED_APPS = ThunderbirdOverrides.INSTALLED_APPS + Staging.INSTALLED_APPS
35 | FRONTEND = ThunderbirdOverrides.FRONTEND
36 | CURRENCIES = ThunderbirdOverrides.CURRENCIES
37 | ENABLE_THUNDERBIRD_REDIRECT = False
38 | FRAUD_SITE_ID = 'tbird'
39 | SALESFORCE_CASE_RECORD_TYPE_ID = "0124x000000A2Pl"
40 |
41 | @property
42 | def TEMPLATES(self):
43 | config = Staging.TEMPLATES
44 | config[0]['DIRS'] = ThunderbirdOverrides.TEMPLATES_DIR + config[0]['DIRS']
45 | return config
46 |
47 | @classmethod
48 | def pre_setup(cls):
49 | super().pre_setup()
50 |
51 | @classmethod
52 | def setup(cls):
53 | super().setup()
54 |
55 | @classmethod
56 | def post_setup(cls):
57 | super().post_setup()
58 |
--------------------------------------------------------------------------------
/source/sass/base/_typography.scss:
--------------------------------------------------------------------------------
1 | /*------------------------------------*\
2 | $TYPOGRAPHY
3 | \*------------------------------------*/
4 |
5 | /* ============================================
6 | Base font
7 | */
8 | html {
9 | font-size: ($base-font-size / 16px) * 100%;
10 | line-height: $base-line-height--mobile;
11 | color: $color--default;
12 |
13 | @include media-query(tablet-portrait) {
14 | line-height: $base-line-height;
15 | }
16 | }
17 |
18 | /* ============================================
19 | Families – one mixin per typeface
20 | :: For each font mixin defined here, make sure each property is negated (set
21 | :: to its default value, usually `inherit`) in all other font mixins.
22 | */
23 | @mixin heading-text() {
24 | font-family: $font--primary;
25 | font-style: inherit;
26 | font-weight: inherit;
27 | }
28 |
29 | @mixin body-text() {
30 | font-family: $font--secondary;
31 | font-style: inherit;
32 | font-weight: inherit;
33 | text-transform: inherit;
34 | }
35 |
36 | @mixin quote-text() {
37 | font-family: $font--secondary;
38 | font-style: italic;
39 | font-weight: inherit;
40 | text-transform: inherit;
41 | }
42 |
43 | // sass-lint:disable single-line-per-selector
44 | h1,
45 | h2,
46 | h3,
47 | h4,
48 | h5,
49 | h6 {
50 | @include heading-text();
51 | }
52 | // sass-lint:enddisable
53 |
54 | html,
55 | .body-text {
56 | @include body-text();
57 | }
58 |
59 | blockquote {
60 | @include quote-text();
61 | }
62 |
63 | p {
64 | margin: 0 0 $gutter;
65 | }
66 |
67 | // Default sizes
68 | h1 {
69 | @include font-size(xl);
70 | }
71 | h2 {
72 | @include font-size(l);
73 | }
74 | h3,
75 | h4,
76 | h5,
77 | h6 {
78 | @include font-size(m);
79 | }
80 | small {
81 | @include font-size(xxs);
82 | }
83 |
84 | .minimum {
85 | @include font-size(xxs);
86 | color: $color--grey-60;
87 | margin-bottom: 0;
88 | }
89 |
--------------------------------------------------------------------------------
/donate/core/tests/test_templatetags.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase, RequestFactory
2 |
3 | from ..templatetags.util_tags import format_currency, get_localized_currency_symbol, to_known_locale
4 |
5 |
6 | class UtilTagsTestCase(TestCase):
7 | """
8 | These are integration tests that sanity check the output of key template tags.
9 | """
10 |
11 | def setUp(self):
12 | self.request = RequestFactory().get('/')
13 |
14 | def test_to_known_locale_fallback_map(self):
15 | self.assertEqual(to_known_locale('es-XL'), 'es')
16 |
17 | def test_format_currency_usd_en_us_integer(self):
18 | value = format_currency('en-US', 'usd', 1)
19 | self.assertEqual(value, '$1')
20 |
21 | def test_format_currency_usd_en_us_decimal(self):
22 | value = format_currency('en-US', 'usd', 1.5)
23 | self.assertEqual(value, '$1.50')
24 |
25 | def test_format_currency_usd_en_gb(self):
26 | value = format_currency('en-GB', 'usd', 1)
27 | self.assertEqual(value, 'US$1')
28 |
29 | def test_format_currency_aed_en_us(self):
30 | value = format_currency('en-US', 'aed', 1)
31 | self.assertEqual(value, 'AED1')
32 |
33 | def test_format_currency_aed_ar(self):
34 | value = format_currency('ar', 'aed', 1)
35 | self.assertEqual(value, 'د.إ. 1')
36 |
37 | def test_get_localised_symbol_usd_en_us(self):
38 | self.request.LANGUAGE_CODE = 'en-US'
39 | ctx = {
40 | 'request': self.request
41 | }
42 | value = get_localized_currency_symbol(ctx, 'usd')
43 | self.assertEqual(value, '$')
44 |
45 | def test_get_localised_symbol_usd_en_gb(self):
46 | self.request.LANGUAGE_CODE = 'en-GB'
47 | ctx = {
48 | 'request': self.request
49 | }
50 | value = get_localized_currency_symbol(ctx, 'usd')
51 | self.assertEqual(value, 'US$')
52 |
--------------------------------------------------------------------------------
/docs/pages.md:
--------------------------------------------------------------------------------
1 | # Pages
2 |
3 | Content on this site is managed using the [Wagtail CMS](https://wagtail.io).
4 | There are just two content types used.
5 |
6 | ## Landing page
7 |
8 | The `LandingPage` content type is used for the root page of the site. It acts
9 | as the home page, and provides a donation form for all non-specific donations.
10 |
11 | The configurable content on this page is:
12 |
13 | - Page title
14 | - Introductory text
15 | - Featured image that appears above introductory text
16 |
17 | Additional settings on this page are:
18 |
19 | - Meta title
20 | - Meta description
21 | - Campaign ID (used for associating donations with a campaign in Basket)
22 | - Project (one of `mozillafoundation` or `thunderbird`, used for associating donations with a project in Basket)
23 |
24 | ## Campaign page
25 |
26 | The `CampaignPage` content type can be created as a child of a `LandingPage`,
27 | and is used to provide a customized, mission-specific donation page.
28 |
29 | The configurable content on this page is:
30 |
31 | - Page title
32 | - Introductory text
33 | - Hero image that appears behind the page title
34 | - Featured image that appears above introductory text
35 |
36 | In addition, the default suggested donation amounts for each currency can be
37 | overridden for a campaign. For example, on Pi Day, you could specify an override
38 | for the USD currency such that the suggested donation amounts for USD are $3.14,
39 | $6.28 and $9.42.
40 |
41 | Any currency for which overrides are not specified will fall back to the global
42 | defaults. The number of suggested amounts is also flexible - 1 to 5 options are
43 | allowed.
44 |
45 | Additional settings on this page are:
46 |
47 | - Meta title
48 | - Meta description
49 | - Campaign ID (used for associating donations with a campaign in Basket)
50 | - Project (one of `mozillafoundation` or `thunderbird`, used for associating donations with a project in Basket)
51 |
--------------------------------------------------------------------------------
/donate/views.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.http import JsonResponse, HttpResponse
3 | from django.views import View
4 | from django.views.decorators.http import require_GET
5 | from django.views.generic.base import RedirectView
6 | from django.shortcuts import render
7 | from donate.payments import constants
8 |
9 |
10 | class EnvVariablesView(View):
11 | """
12 | A view that permits a GET to expose whitelisted environment
13 | variables in JSON.
14 | """
15 |
16 | def get(self, request):
17 | return JsonResponse(settings.FRONTEND)
18 |
19 |
20 | class WaysToGiveView(View):
21 | """
22 | A view for "ways to give" page
23 | """
24 |
25 | def get(self, request):
26 | return render(request, 'pages/core/ways_to_give_page.html', {
27 | 'currencies': constants.CURRENCIES,
28 | 'ways_to_give_link':
29 | request.scheme + "://" + request.get_host() + "/" + request.LANGUAGE_CODE
30 | + "/?utm_content=Ways_to_Give",
31 | })
32 |
33 |
34 | class ThunderbirdRedirectView(RedirectView):
35 | """
36 | A view that redirects requests to give.thunderbird.net, preserving query params
37 | """
38 |
39 | url = 'https://give.thunderbird.net/'
40 | permanent = False
41 | query_string = True
42 |
43 |
44 | @require_GET
45 | def apple_pay_domain_association_view(request):
46 | """
47 | Returns string needed for Apple Pay domain association/verification
48 | """
49 | apple_pay_key = settings.APPLE_PAY_DOMAIN_ASSOCIATION_KEY
50 | key_not_found_message = "Key not found. Please check environment variables."
51 |
52 | if apple_pay_key:
53 | response_contents = apple_pay_key
54 | status_code = 200
55 | else:
56 | response_contents = key_not_found_message
57 | status_code = 501
58 |
59 | return HttpResponse(response_contents, status=status_code, content_type="text/plain; charset=utf-8")
60 |
--------------------------------------------------------------------------------
/donate/settings/production.py:
--------------------------------------------------------------------------------
1 | from configurations import Configuration
2 | from .base import Base
3 | from .secure import Secure
4 | from .oidc import OIDC
5 | from .database import Database
6 | from .redis import Redis
7 | from .s3 import S3
8 | from .salesforce import Salesforce
9 | from .braintree import Braintree
10 | from .sentry import Sentry
11 | from .thunderbird import ThunderbirdOverrides
12 |
13 |
14 | class Production(Base, Secure, OIDC, Database, Redis, S3, Salesforce, Braintree, Sentry, Configuration):
15 | DEBUG = False
16 | SALESFORCE_CASE_RECORD_TYPE_ID = "0124x000000A1ez"
17 |
18 | @classmethod
19 | def pre_setup(cls):
20 | super().pre_setup()
21 | # production forces debug to be False, so the rq worker will never log donor data with the config below.
22 | cls.LOGGING['loggers']['rq.worker']['handlers'] = ['debug']
23 | cls.LOGGING['loggers']['rq.worker']['level'] = 'DEBUG'
24 |
25 | @classmethod
26 | def setup(cls):
27 | super().setup()
28 |
29 | @classmethod
30 | def post_setup(cls):
31 | super().post_setup()
32 |
33 |
34 | class ThunderbirdProduction(Production, ThunderbirdOverrides, Configuration):
35 | INSTALLED_APPS = ThunderbirdOverrides.INSTALLED_APPS + Production.INSTALLED_APPS
36 | FRONTEND = ThunderbirdOverrides.FRONTEND
37 | CURRENCIES = ThunderbirdOverrides.CURRENCIES
38 | ENABLE_THUNDERBIRD_REDIRECT = False
39 | SALESFORCE_CASE_RECORD_TYPE_ID = "0124x000000A2Pl"
40 | FRAUD_SITE_ID = 'tbird'
41 |
42 | @property
43 | def TEMPLATES(self):
44 | config = Production.TEMPLATES
45 | config[0]['DIRS'] = ThunderbirdOverrides.TEMPLATES_DIR + config[0]['DIRS']
46 | return config
47 |
48 | @classmethod
49 | def pre_setup(cls):
50 | super().pre_setup()
51 |
52 | @classmethod
53 | def setup(cls):
54 | super().setup()
55 |
56 | @classmethod
57 | def post_setup(cls):
58 | super().post_setup()
59 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | .DS_Store
3 | TODO.txt
4 | node_modules/
5 | dest/
6 |
7 | # Byte-compiled / optimized / DLL files
8 | __pycache__/
9 | *.py[cod]
10 | *$py.class
11 |
12 | # C extensions
13 | *.so
14 |
15 | # Distribution / packaging
16 | .Python
17 | env/
18 | build/
19 | develop-eggs/
20 | dist/
21 | downloads/
22 | eggs/
23 | .eggs/
24 | lib/
25 | lib64/
26 | parts/
27 | sdist/
28 | var/
29 | *.egg-info/
30 | .installed.cfg
31 | *.egg
32 |
33 | # PyInstaller
34 | # Usually these files are written by a python script from a template
35 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
36 | *.manifest
37 | *.spec
38 |
39 | # Installer logs
40 | pip-log.txt
41 | pip-delete-this-directory.txt
42 |
43 | # Unit test / coverage reports
44 | htmlcov/
45 | .tox/
46 | .coverage
47 | .coverage.*
48 | .cache
49 | nosetests.xml
50 | coverage.xml
51 | *,cover
52 | .hypothesis/
53 |
54 | # Translations
55 | *.po
56 | *.pot
57 | *.mo
58 | *.ftl
59 | translations_github_commit_*
60 | translations.tar
61 |
62 | # Django stuff:
63 | *.log
64 | local_settings.py
65 |
66 | # IPython Notebook
67 | .ipynb_checkpoints
68 |
69 | # pyenv
70 | .python-version
71 |
72 | # dotenv
73 | .env
74 | .docker.env
75 |
76 | # virtualenv
77 | venv/
78 | ENV/
79 |
80 | # Spyder project settings
81 | .spyderproject
82 |
83 | # Rope project settings
84 | .ropeproject
85 |
86 | # Mac Garbage
87 | **/.DS_Store
88 |
89 | # Static files from python manage.py collectstatic
90 | /static/
91 |
92 | # Storage for user generated files
93 | /media/
94 |
95 | # SQLite3 databases for local development
96 | **/*.sqlite3
97 |
98 | # Compiled Frontend
99 | donate/frontend/*
100 |
101 | # PyCharm
102 | .idea/
103 |
104 | # Cypress visual regression testing
105 | cypress/videos
106 |
107 | # VSCode project config
108 | .vscode/*
109 | !.vscode/launch.json
110 |
111 | # Python virtualenv
112 | pythondockervenv/
113 |
--------------------------------------------------------------------------------
/donate/payments/constants.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 |
3 |
4 | DEFAULT_FRU_FORM_ID = '#XFJLGDNG'
5 |
6 | FREQUENCY_SINGLE = 'single'
7 | FREQUENCY_MONTHLY = 'monthly'
8 |
9 | FREQUENCIES = (FREQUENCY_SINGLE, FREQUENCY_MONTHLY)
10 |
11 | FREQUENCY_CHOICES = (
12 | (FREQUENCY_SINGLE, 'Single'),
13 | (FREQUENCY_MONTHLY, 'Monthly'),
14 | )
15 |
16 | METHOD_CARD = 'Braintree_Card'
17 | METHOD_PAYPAL = 'Braintree_Paypal'
18 |
19 | PAYPAL_ACCOUNT_MICRO = 'micro'
20 | PAYPAL_ACCOUNT_MACRO = 'macro'
21 |
22 | METHODS = (METHOD_CARD, METHOD_PAYPAL)
23 |
24 |
25 | CURRENCIES = settings.CURRENCIES
26 |
27 | CURRENCY_CHOICES = tuple([
28 | (key, '{} {}'.format(key.upper(), data['symbol'])) for key, data in CURRENCIES.items()
29 | ])
30 |
31 |
32 | LOCALE_CURRENCY_MAP = {
33 | 'ast': 'eur',
34 | 'ca': 'eur',
35 | 'cs': 'czk',
36 | 'cy': 'gbp',
37 | 'da': 'dkk',
38 | 'de': 'eur',
39 | 'dsb': 'eur',
40 | 'el': 'eur',
41 | 'en-AU': 'aud',
42 | 'en-CA': 'cad',
43 | 'en-GB': 'gbp',
44 | 'en-IN': 'inr',
45 | 'en-NZ': 'nzd',
46 | 'en-US': 'usd',
47 | 'es': 'eur',
48 | 'es-MX': 'mxn',
49 | 'es-XL': 'mxn',
50 | 'et': 'eur',
51 | 'fi': 'eur',
52 | 'fr': 'eur',
53 | 'fy-NL': 'eur',
54 | 'gu-IN': 'inr',
55 | 'hi-IN': 'inr',
56 | 'hr': 'eur',
57 | 'hsb': 'eur',
58 | 'hu': 'huf',
59 | 'it': 'eur',
60 | 'ja': 'jpy',
61 | 'kab': 'eur',
62 | 'lv': 'eur',
63 | 'ml': 'inr',
64 | 'mr': 'inr',
65 | 'nb-NO': 'nok',
66 | 'nl': 'eur',
67 | 'nn-NO': 'nok',
68 | 'pa-IN': 'inr',
69 | 'pl': 'pln',
70 | 'pt-BR': 'brl',
71 | 'pt-PT': 'eur',
72 | 'ru': 'rub',
73 | 'sk': 'eur',
74 | 'sl': 'eur',
75 | 'sv-SE': 'sek',
76 | 'ta': 'inr',
77 | 'te': 'inr',
78 | 'zh-TW': 'twd'
79 | }
80 |
81 | ZERO_DECIMAL_CURRENCIES = [
82 | 'BIF',
83 | 'CLP',
84 | 'DJF',
85 | 'GNF',
86 | 'JPY',
87 | 'KMF',
88 | 'KRW',
89 | 'MGA',
90 | 'PYG',
91 | 'RWF',
92 | 'VND',
93 | 'VUV',
94 | 'XAF',
95 | 'XOF',
96 | 'XPF'
97 | ]
98 |
--------------------------------------------------------------------------------
/donate/core/pontoon.py:
--------------------------------------------------------------------------------
1 | import django_rq
2 | from django.conf import settings
3 | from redlock import RedLock, RedLockError
4 |
5 | from wagtail_localize_git.sync import SyncManager
6 |
7 |
8 | def _sync_task():
9 | CustomSyncManager().sync()
10 |
11 |
12 | class CustomSyncManager(SyncManager):
13 | """
14 | Implements sync/trigger/is_queued/is_running.
15 |
16 | These are called from various parts of wagtail-localize-pontoon package.
17 | """
18 |
19 | def __init__(self):
20 | super().__init__()
21 |
22 | self.queue = django_rq.get_queue('wagtail_localize_pontoon.sync')
23 |
24 | def lock(self):
25 | connection_info = {
26 | 'url': settings.REDIS_URL,
27 | }
28 |
29 | if settings.REDIS_URL.startswith("rediss"):
30 | connection_info["ssl_cert_reqs"] = None
31 |
32 | return RedLock("lock:wagtail_localize_pontoon.sync", connection_details=[
33 | connection_info
34 | ])
35 |
36 | def sync(self):
37 | """
38 | Performs the synchronisation
39 |
40 | Called directly from the sync_pontoon command
41 | """
42 | try:
43 | with self.lock():
44 | super().sync()
45 |
46 | except RedLockError:
47 | self.logger.warning("Failed to acquire lock. The task is probably already running.")
48 |
49 | def trigger(self):
50 | """
51 | Enqueues a job to perform synchronisation in the background
52 |
53 | Called by Django view when "Force sync" is pressed
54 | """
55 | # Make sure there is only one job in the queue at a time
56 | self.queue.delete()
57 | self.queue.enqueue(_sync_task)
58 |
59 | def is_queued(self):
60 | """
61 | Returns True if a synchronisation job is already in the queue
62 | """
63 | return bool(self.queue.get_jobs())
64 |
65 | def is_running(self):
66 | """
67 | Returns True if a synchronisation job is running right now
68 | """
69 | return self.lock().locked()
70 |
--------------------------------------------------------------------------------
/donate/settings/development.py:
--------------------------------------------------------------------------------
1 | from configurations import Configuration
2 | from .environment import env
3 | from .secure import Secure
4 | from .oidc import OIDC
5 | from .database import Database
6 | from .base import Base
7 | from .redis import Redis
8 | from .braintree import Braintree
9 | from .s3 import S3
10 | from .salesforce import Salesforce
11 | from .thunderbird import ThunderbirdOverrides
12 |
13 |
14 | class Development(Base, Secure, OIDC, Database, Redis, S3, Salesforce, Braintree, Configuration):
15 | DEBUG = env('DEBUG')
16 | DJANGO_LOG_LEVEL = env('DJANGO_LOG_LEVEL')
17 | SALESFORCE_CASE_RECORD_TYPE_ID = "0124x000000A1ez"
18 |
19 | SECURE_SSL_REDIRECT = False
20 | SESSION_COOKIE_SECURE = False
21 | CSRF_COOKIE_SECURE = False
22 | USE_X_FORWARDED_HOST = False
23 | SECURE_HSTS_INCLUDE_SUBDOMAINS = False
24 |
25 | # In dev, lets not worry about enforcing password changes
26 | AUTH_PASSWORD_VALIDATORS = []
27 |
28 | @classmethod
29 | def pre_setup(cls):
30 | super().pre_setup()
31 |
32 | @classmethod
33 | def setup(cls):
34 | super().setup()
35 |
36 | @classmethod
37 | def post_setup(cls):
38 | super().post_setup()
39 |
40 |
41 | class ThunderbirdDevelopment(Development, ThunderbirdOverrides, Configuration):
42 | INSTALLED_APPS = ThunderbirdOverrides.INSTALLED_APPS + Development.INSTALLED_APPS
43 | FRONTEND = ThunderbirdOverrides.FRONTEND
44 | CURRENCIES = ThunderbirdOverrides.CURRENCIES
45 | ENABLE_THUNDERBIRD_REDIRECT = False
46 | SALESFORCE_CASE_RECORD_TYPE_ID = "0124x000000A2Pl"
47 | FRAUD_SITE_ID = 'tbird'
48 |
49 | @property
50 | def TEMPLATES(self):
51 | config = Development.TEMPLATES
52 | config[0]['DIRS'] = ThunderbirdOverrides.TEMPLATES_DIR + config[0]['DIRS']
53 | return config
54 |
55 | @classmethod
56 | def pre_setup(cls):
57 | super().pre_setup()
58 |
59 | @classmethod
60 | def setup(cls):
61 | super().setup()
62 |
63 | @classmethod
64 | def post_setup(cls):
65 | super().post_setup()
66 |
--------------------------------------------------------------------------------
/donate/thunderbird/templates/fragments/donate_form_disclaimer.html:
--------------------------------------------------------------------------------
1 | {% extends "fragments/donate_form_disclaimer_master.html" %}
2 |
3 | {% load i18n %}
4 |
5 | {% block other_options %}
6 |
7 |
8 | {% blocktrans with check_url='/ways-to-give#check' %}Other ways to give: Check {% endblocktrans %}
9 |
10 |
11 | {% endblock %}
12 |
13 | {% block donation_information %}
14 |
15 | {% blocktrans with braintree_url='https://www.braintreepayments.com/legal/braintree-privacy-policy' paypal_url='https://www.paypal.com/us/webapps/mpp/ua/privacy-full' privacy_url="https://www.mozilla.org/privacy/websites/" trimmed %}
16 | Your payment details will be processed by Braintree , a PayPal company (for credit/debit cards) or PayPal , and a record of your donation will be stored by Thunderbird. Please read our privacy policy here .
17 | {% endblocktrans %}
18 | {% if monthly %}{% blocktrans with help_url='/help' trimmed %}
19 | Monthly donations are charged each month on the same day that you donate today, and will continue until you cancel. To cancel, fill out this form .
20 | {% endblocktrans %}{% endif %}
21 |
22 | {% endblock %}
23 |
24 | {% block faq_contact %}
25 |
26 | {% blocktrans with faq_url='/faq' help_url='/help' trimmed %}
27 | Problems donating? Visit our FAQ for answers to most common questions. Still have problems? Contact us .
28 | {% endblocktrans %}
29 |
30 | {% endblock %}
31 |
32 | {% block contribution_statement %}
33 |
34 | {% blocktrans %}Contributions go to MZLA Technologies Corporation, a California corporation wholly owned by the Mozilla Foundation. Funds will be reserved for use in the Thunderbird project. Contributions are not tax-deductible as charitable contributions.{% endblocktrans %}
35 |
36 | {% endblock %}
37 |
--------------------------------------------------------------------------------
/source/js/components/validation.js:
--------------------------------------------------------------------------------
1 | // Handler for "other amount" inputs
2 | function otherAmountInputValidation() {
3 | // constants for "Other Amount" inputs and error messages
4 | var oneTimeOtherAmountInput = document.getElementById(
5 | "one-time-amount-other-input"
6 | ),
7 | monthlyOtherAmountInput = document.getElementById(
8 | "monthly-amount-other-input"
9 | ),
10 | oneTimeOtherAmountErrorMessage = document.getElementById(
11 | "other-one-time-amount-error-message"
12 | ),
13 | monthlyOtherAmountErrorMessage = document.getElementById(
14 | "other-monthly-amount-error-message"
15 | );
16 | if (oneTimeOtherAmountInput) {
17 | oneTimeOtherAmountInput.addEventListener("blur", (e) => {
18 | if (document.querySelector(".one-time-amount-donation-radio:checked")) {
19 | inputValueCheck(
20 | oneTimeOtherAmountInput,
21 | oneTimeOtherAmountErrorMessage
22 | );
23 | }
24 | });
25 | }
26 | if (monthlyOtherAmountInput) {
27 | monthlyOtherAmountInput.addEventListener("blur", (e) => {
28 | if (document.querySelector(".monthly-amount-donation-radio:checked")) {
29 | inputValueCheck(
30 | monthlyOtherAmountInput,
31 | monthlyOtherAmountErrorMessage
32 | );
33 | }
34 | });
35 | }
36 | }
37 |
38 | function inputValueCheck(inputObjectToValidate, messageToUpdate) {
39 | const number = parseFloat(inputObjectToValidate.value);
40 | const minimumAmount = parseFloat(inputObjectToValidate.min);
41 | const maximumAmount = parseFloat(inputObjectToValidate.max);
42 | // We are getting the minimum and maximum amount for input above as these can change based on currency
43 | if (
44 | !isNaN(number) &&
45 | number &&
46 | number >= minimumAmount &&
47 | number <= maximumAmount
48 | ) {
49 | // hide error message
50 | messageToUpdate.classList.add("hidden");
51 | } else {
52 | // display error message
53 | messageToUpdate.classList.remove("hidden");
54 | }
55 | }
56 |
57 | export default otherAmountInputValidation;
58 |
--------------------------------------------------------------------------------
/source/sass/components/_donation-amount.scss:
--------------------------------------------------------------------------------
1 | .donation-amount {
2 | $root: &;
3 |
4 | &--two-col {
5 | grid-column: span 2;
6 | }
7 |
8 | &--other {
9 | display: flex;
10 | flex-direction: row;
11 |
12 | #donate-form--monthly & {
13 | @include media-query(tablet-portrait) {
14 | height: 100%;
15 | max-height: 90px;
16 | }
17 | }
18 | }
19 |
20 | &__container {
21 | position: relative;
22 | }
23 |
24 | &__label {
25 | position: relative;
26 | @include z-index(base);
27 | display: block;
28 | border: 1px solid $color--border;
29 | padding: 20px 5px;
30 | text-align: center;
31 | cursor: pointer;
32 | transition: color $transition, border-color $transition,
33 | background-color $transition;
34 |
35 | span {
36 | @include font-size(xs);
37 | display: block;
38 | }
39 | #{$root}--other & {
40 | flex-basis: 55px;
41 | min-width: 55px;
42 | max-width: 55px;
43 | }
44 | #donate-form--monthly #{$root}--other & {
45 | padding-top: 22px;
46 |
47 | @include media-query(tablet-portrait) {
48 | padding-top: 32px;
49 | }
50 | }
51 |
52 | &--hidden {
53 | @include hidden();
54 | }
55 | }
56 |
57 | &__radio {
58 | position: absolute;
59 | @include z-index(hidden);
60 |
61 | &:checked + #{$root}__label,
62 | &:focus + #{$root}__label {
63 | background-color: $color--primary;
64 | border-color: $color--primary;
65 | color: $color--white;
66 | }
67 | }
68 |
69 | &__input:not([type="checkbox"]):not([type="radio"]) {
70 | #{$root}--other & {
71 | flex-grow: 1;
72 | padding-left: ($gutter * 0.75);
73 | border: 1px solid $color--border;
74 | border-left: 0;
75 | -moz-appearance: textfield;
76 |
77 | &::-webkit-outer-spin-button,
78 | &::-webkit-inner-spin-button {
79 | -webkit-appearance: none;
80 | margin: 0;
81 | }
82 |
83 | &:invalid {
84 | box-shadow: none;
85 | border-color: $color--warning;
86 | }
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/source/sass/components/_header.scss:
--------------------------------------------------------------------------------
1 | .header {
2 | $root: &;
3 | @include z-index(header);
4 | $logo-width-desktop: 97px;
5 | $logo-height-desktop: 28px;
6 | $logo-width-mobile: 28px;
7 | $logo-height-mobile: 28px;
8 | position: relative;
9 | background-color: $color--white;
10 | border-bottom: 1px solid $color--border;
11 |
12 | &__container {
13 | position: relative;
14 | display: flex;
15 | flex-direction: row;
16 | align-items: center;
17 | padding: 24px $gutter;
18 | margin: 0 auto;
19 |
20 | @include media-query(tablet-portrait) {
21 | padding: 24px ($gutter);
22 | }
23 |
24 | @include media-query(desktop) {
25 | padding: 24px $gutter;
26 | max-width: (
27 | $site-width--default + (20 * 2)
28 | ); // Allow for padding on container
29 | }
30 | }
31 |
32 | &__logo-link {
33 | display: block;
34 | width: $logo-width-mobile;
35 | height: $logo-height-mobile;
36 |
37 | @include media-query(tablet-portrait) {
38 | width: $logo-width-desktop;
39 | height: $logo-height-desktop;
40 | }
41 | }
42 |
43 | &__logo {
44 | display: block;
45 | width: $logo-width-mobile;
46 | height: $logo-height-mobile;
47 | background-color: $color--black;
48 |
49 | @include media-query(tablet-portrait) {
50 | width: $logo-width-desktop;
51 | height: $logo-height-desktop;
52 | }
53 |
54 | &--desktop {
55 | display: none;
56 |
57 | @include media-query(tablet-portrait) {
58 | display: block;
59 | }
60 | }
61 |
62 | &--mobile {
63 | display: block;
64 |
65 | @include media-query(tablet-portrait) {
66 | display: none;
67 | }
68 | }
69 | }
70 |
71 | &__menu-toggle {
72 | display: flex;
73 | width: 28px;
74 | height: 28px;
75 | margin-right: ($gutter);
76 |
77 | @include media-query(desktop) {
78 | position: absolute;
79 | left: -28px;
80 | }
81 | }
82 |
83 | &__menu-label {
84 | @include hidden();
85 | }
86 |
87 | .app--no-menu & {
88 | #{$root}__menu-toggle {
89 | display: none;
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | # Run the wagtail app
5 | backend:
6 | build:
7 | context: .
8 | dockerfile: ./dockerfiles/Dockerfile.python
9 | env_file:
10 | - ".env"
11 | command: ./dockerpythonvenv/bin/python manage.py runserver 0.0.0.0:8000
12 | ports:
13 | - "8000:8000"
14 | - "8001:8001" # ptvsd port for debugging
15 | volumes:
16 | - .:/app:delegated
17 | - dockerpythonvenv:/app/dockerpythonvenv/:delegated
18 | depends_on:
19 | - postgres
20 | - redis
21 | - watch-static-files
22 | links:
23 | - redis
24 |
25 | # RQ Worker to process donations
26 | backend-worker:
27 | build:
28 | context: .
29 | dockerfile: ./dockerfiles/Dockerfile.python
30 | command: ./dockerpythonvenv/bin/python manage.py rqworker default
31 | volumes:
32 | - .:/app:delegated
33 | - dockerpythonvenv:/app/dockerpythonvenv/:delegated
34 | depends_on:
35 | - postgres
36 | - redis
37 | links:
38 | - redis
39 |
40 | # Database
41 | postgres:
42 | environment:
43 | - POSTGRES_DB=donate
44 | - POSTGRES_USER=donate
45 | # We're only using this setting for local dev!
46 | - POSTGRES_HOST_AUTH_METHOD=trust
47 | image: postgres:11
48 | ports:
49 | - "5678:5432"
50 | volumes:
51 | - postgres_data:/var/lib/postgresql/data/:delegated
52 |
53 | redis:
54 | image: redis
55 | container_name: 'redis'
56 | ports:
57 | - "6380:6379"
58 |
59 | # Rebuild static files automatically
60 | watch-static-files:
61 | build:
62 | context: .
63 | dockerfile: ./dockerfiles/Dockerfile.node
64 | env_file:
65 | - .env
66 | environment:
67 | # Need to specify the SHELL env var for chokidar
68 | - SHELL=/bin/sh
69 | # Force polling because inotify doesn't work on Docker Windows
70 | - CHOKIDAR_USEPOLLING=1
71 | command: npm run watch
72 | volumes:
73 | - .:/app:delegated
74 | - node_dependencies:/app/node_modules/:delegated
75 |
76 | volumes:
77 | postgres_data:
78 | node_dependencies:
79 | dockerpythonvenv:
80 |
--------------------------------------------------------------------------------
/donate/settings/oidc.py:
--------------------------------------------------------------------------------
1 | from .environment import env
2 |
3 |
4 | # Mozilla OpenID Connect/Auth0 configuration
5 | class OIDC(object):
6 | # disable user creating during authentication
7 | OIDC_CREATE_USER = False
8 |
9 | # How frequently do we check with the provider that the user still exists.
10 | OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS = 15 * 60
11 |
12 | OIDC_RP_SIGN_ALGO = "RS256"
13 | OIDC_RP_CLIENT_ID = env("OIDC_RP_CLIENT_ID")
14 | OIDC_RP_CLIENT_SECRET = env("OIDC_RP_CLIENT_SECRET")
15 |
16 | # These values should be overwritten by thunderbird/other instances:
17 | OIDC_OP_AUTHORIZATION_ENDPOINT = "https://auth.mozilla.auth0.com/authorize"
18 | OIDC_OP_TOKEN_ENDPOINT = "https://auth.mozilla.auth0.com/oauth/token"
19 | OIDC_OP_USER_ENDPOINT = "https://auth.mozilla.auth0.com/userinfo"
20 | OIDC_OP_DOMAIN = "auth.mozilla.auth0.com"
21 | OIDC_OP_JWKS_ENDPOINT = "https://auth.mozilla.auth0.com/.well-known/jwks.json"
22 |
23 | LOGIN_REDIRECT_URL = "/admin/"
24 | LOGOUT_REDIRECT_URL = "/"
25 |
26 | # If True (which should only be done in settings.local), then show username and
27 | # password fields. You'll also need to enable the model backend in local settings
28 | USE_CONVENTIONAL_AUTH = env('USE_CONVENTIONAL_AUTH')
29 |
30 | # Extra Wagtail config to disable password usage (SSO should be the only way in)
31 | # https://docs.wagtail.io/en/v2.6.3/advanced_topics/settings.html#password-management
32 | # Don't let users change or reset their password
33 | WAGTAIL_PASSWORD_MANAGEMENT_ENABLED = False
34 | WAGTAIL_PASSWORD_RESET_ENABLED = False
35 |
36 | # Don't require a password when creating a user,
37 | # and blank password means cannot log in unless SSO
38 | WAGTAILUSERS_PASSWORD_ENABLED = False
39 |
40 | @classmethod
41 | def setup(cls):
42 | # EXTRA LOGGING
43 | cls.LOGGING['loggers']['mozilla_django_oidc'] = {
44 | 'handlers': ['debug'],
45 | 'level': 'INFO',
46 | }
47 |
48 | if cls.USE_CONVENTIONAL_AUTH is False:
49 | cls.AUTHENTICATION_BACKENDS = (
50 | 'mozilla_django_oidc.auth.OIDCAuthenticationBackend',
51 | )
52 |
--------------------------------------------------------------------------------
/donate/templates/blocks/accordion_block.html:
--------------------------------------------------------------------------------
1 | {% load wagtailcore_tags %}
2 |
3 |
4 |
5 |
{{ value.title }}
6 |
7 |
8 | {% for item in value.items %}
9 |
43 | {% endfor %}
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/donate/templates/fragments/donate_form_disclaimer_fundraise_up.html:
--------------------------------------------------------------------------------
1 | {% extends "fragments/donate_form_disclaimer_master.html" %}
2 |
3 | {% load i18n util_tags %}
4 |
5 |
6 | {% block donation_information %}
7 |
8 | {% blocktrans with stripe_url='https://stripe.com/en-ca/legal/privacy-center' paypal_url='https://www.paypal.com/us/webapps/mpp/ua/privacy-full' privacy_url="https://www.mozilla.org/privacy/websites/" trimmed %}
9 | Mozilla is committed to your privacy; please read our privacy policy here . Your payment details will be processed by Stripe , or PayPal , and a record of your donation will be stored by Mozilla.
10 | {% endblocktrans %}
11 |
12 | {% endblock %}
13 |
14 | {% block other_options %}
15 |
16 | {% blocktrans with ways_to_give_url='https://foundation.mozilla.org/donate/ways-to-give' trimmed %}
17 | Other ways to give: Check, Bank Transfer, Stocks
18 | {% endblocktrans %}
19 |
20 | {% endblock %}
21 |
22 | {% block faq_contact %}
23 |
24 | {% blocktrans with faq_url='https://foundation.mozilla.org/donate/help/#frequently-asked-questions' trimmed %}
25 | Question donating? Visit our FAQ for answers to most common questions.
26 | {% endblocktrans %}
27 |
28 |
29 | {% blocktrans with help_url='https://foundation.mozilla.org/donate/help' trimmed %}
30 | Need to reach us about your donation? Contact us .
31 | {% endblocktrans %}
32 |
33 | {% endblock %}
34 |
35 | {% block contribution_statement %}
36 |
37 | {% blocktrans with mozilla_url='https://foundation.mozilla.org/who-we-are/' trimmed %}
38 | Contributions go to the Mozilla Foundation , a 501(c)(3) organization based in San Francisco, California, to be used in its discretion for its charitable purposes. They are tax-deductible in the U.S. to the fullest extent permitted by law.
39 | {% endblocktrans %}
40 |
41 | {% endblock %}
42 |
--------------------------------------------------------------------------------
/source/sass/components/_form-custom-select.scss:
--------------------------------------------------------------------------------
1 | $select-height: 57px;
2 | $select-height--small: 37px;
3 |
4 | .form-custom-select {
5 | position: relative;
6 | width: 100%;
7 | background-color: $color--white;
8 |
9 | &:active,
10 | &:focus {
11 | border-color: $color--primary;
12 | }
13 |
14 | &::after {
15 | content: "";
16 | position: absolute;
17 | top: 0;
18 | right: 0;
19 | bottom: 0;
20 | z-index: 2; /* FIXME: this needs a name, based on _variables.scss */
21 | width: 50px;
22 | height: 7px;
23 | margin: auto 0;
24 | pointer-events: none;
25 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z'/%3E%3Cpath fill='none' d='M0 0h24v24H0V0z'/%3E%3C/svg%3E");
26 | background-repeat: no-repeat;
27 | background-position: center;
28 | background-size: 24px 24px;
29 | }
30 |
31 | &::before {
32 | content: "";
33 | position: absolute;
34 | top: 1px;
35 | right: 1px;
36 | @include z-index(base);
37 | display: block;
38 | width: $select-height;
39 | height: ($select-height - 2px);
40 | pointer-events: none;
41 | background-color: $color--white;
42 | }
43 |
44 | select {
45 | @include font-size(m);
46 | position: relative;
47 | width: 100%;
48 | height: $select-height;
49 | margin: 0;
50 | color: $color--default;
51 | cursor: pointer;
52 | background-color: $color--white;
53 | border: 1px solid $color--border;
54 | outline: none;
55 | font-weight: $weight--normal;
56 | text-indent: 13px;
57 | appearance: none;
58 | border-radius: 0;
59 |
60 | &:active,
61 | &:focus {
62 | border-color: $color--primary;
63 | }
64 | }
65 |
66 | .donate-form__currency & {
67 | max-width: 140px;
68 | min-width: 140px;
69 |
70 | &:before {
71 | width: $select-height--small;
72 | height: ($select-height--small - 2px);
73 | }
74 |
75 | select {
76 | @include font-size(default);
77 | color: $color--black;
78 | height: $select-height--small;
79 | text-indent: 10px;
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/dev-requirements.txt:
--------------------------------------------------------------------------------
1 | #
2 | # This file is autogenerated by pip-compile with python 3.9
3 | # To update, run:
4 | #
5 | # pip-compile dev-requirements.in
6 | #
7 | backcall==0.1.0
8 | # via ipython
9 | certifi==2021.10.8
10 | # via
11 | # -c requirements.txt
12 | # requests
13 | # urllib3
14 | cffi==1.15.0
15 | # via
16 | # -c requirements.txt
17 | # cryptography
18 | charset-normalizer==2.0.9
19 | # via
20 | # -c requirements.txt
21 | # requests
22 | coverage==5.1
23 | # via coveralls
24 | coveralls==2.0.0
25 | # via -r dev-requirements.in
26 | cryptography==36.0.0
27 | # via
28 | # -c requirements.txt
29 | # pyopenssl
30 | # urllib3
31 | decorator==4.4.2
32 | # via
33 | # ipython
34 | # traitlets
35 | docopt==0.6.2
36 | # via coveralls
37 | flake8==3.8.4
38 | # via -r dev-requirements.in
39 | idna==3.3
40 | # via
41 | # -c requirements.txt
42 | # requests
43 | # urllib3
44 | ipython==7.16.1
45 | # via -r dev-requirements.in
46 | ipython-genutils==0.2.0
47 | # via traitlets
48 | jedi==0.17.0
49 | # via ipython
50 | mccabe==0.6.1
51 | # via flake8
52 | parso==0.7.0
53 | # via jedi
54 | pexpect==4.8.0
55 | # via ipython
56 | pickleshare==0.7.5
57 | # via ipython
58 | prompt-toolkit==3.0.5
59 | # via ipython
60 | ptvsd==4.3.2
61 | # via -r dev-requirements.in
62 | ptyprocess==0.6.0
63 | # via pexpect
64 | pycodestyle==2.6.0
65 | # via flake8
66 | pycparser==2.21
67 | # via
68 | # -c requirements.txt
69 | # cffi
70 | pyflakes==2.2.0
71 | # via flake8
72 | pygments==2.6.1
73 | # via ipython
74 | pyopenssl==21.0.0
75 | # via
76 | # -c requirements.txt
77 | # urllib3
78 | requests==2.26.0
79 | # via
80 | # -c requirements.txt
81 | # coveralls
82 | six==1.16.0
83 | # via
84 | # -c requirements.txt
85 | # pyopenssl
86 | # traitlets
87 | traitlets==4.3.3
88 | # via ipython
89 | urllib3[secure]==1.26.7
90 | # via
91 | # -c requirements.txt
92 | # requests
93 | wcwidth==0.1.9
94 | # via prompt-toolkit
95 |
96 | # The following packages are considered to be unsafe in a requirements file:
97 | # setuptools
98 |
--------------------------------------------------------------------------------
/donate/core/__init__.py:
--------------------------------------------------------------------------------
1 | import functools
2 |
3 | import django
4 | from django.utils.translation.trans_real import accept_language_re
5 |
6 | from . import translation # noqa F401
7 |
8 |
9 | # WARNING: this is not necessarily a good idea, but is the only way to override
10 | # Django's default behaviour of requiring language codes to be lowercased.
11 | # We have to modify the core Django method, because we have no way to replace
12 | # all the core functionality that relies on this - e.g., url resolvers that
13 | # the Django admin and third party apps use.
14 | # This is pretty ugly, and ideally this should be fixed upstream.
15 |
16 |
17 | def language_code_to_iso_3166(language):
18 | """Turn a language name (en-us) into an ISO 3166 format (en-US)."""
19 | language, _, country = language.lower().partition('-')
20 | if country:
21 | return language + '-' + country.upper()
22 | return language
23 |
24 |
25 | def to_language(locale):
26 | """Turn a locale name (en_US) into a language name (en-US)."""
27 | return locale.replace('_', '-')
28 |
29 |
30 | @functools.lru_cache(maxsize=1000)
31 | def parse_accept_lang_header(lang_string):
32 | """
33 | Parse the lang_string, which is the body of an HTTP Accept-Language
34 | header, and return a tuple of (lang, q-value), ordered by 'q' values.
35 |
36 | Return an empty tuple if there are any format errors in lang_string.
37 | """
38 | result = []
39 | pieces = accept_language_re.split(lang_string.lower())
40 | if pieces[-1]:
41 | return ()
42 | for i in range(0, len(pieces) - 1, 3):
43 | first, lang, priority = pieces[i:i + 3]
44 | if first:
45 | return ()
46 | if priority:
47 | priority = float(priority)
48 | else:
49 | priority = 1.0
50 | result.append((language_code_to_iso_3166(lang), priority))
51 | result.sort(key=lambda k: k[1], reverse=True)
52 | return tuple(result)
53 |
54 |
55 | # Replace some functions in django.utils.translation.trans_real with our own
56 | # versions that support a language in the form en-US instead of en-us.
57 | django.utils.translation.trans_real.to_language = to_language
58 | django.utils.translation.trans_real.parse_accept_lang_header = parse_accept_lang_header
59 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "donate-wagtail",
3 | "description": "Donate platform",
4 | "repository": "https://github.com/mozilla/donate-wagtail",
5 | "addons": ["heroku-postgresql:mini", "heroku-redis:mini"],
6 | "buildpacks": [
7 | {
8 | "url": "heroku/nodejs"
9 | },
10 | {
11 | "url": "heroku/python"
12 | }
13 | ],
14 | "formation": {
15 | "web": {
16 | "quantity": 1
17 | },
18 | "worker": {
19 | "quantity": 1
20 | }
21 | },
22 | "env": {
23 | "DJANGO_SECRET_KEY": {
24 | "generator": "secret"
25 | },
26 | "DJANGO_SETTINGS_MODULE": "donate.settings",
27 | "BRAINTREE_USE_SANDBOX": "True",
28 | "CONTENT_TYPE_NO_SNIFF": "True",
29 | "DEBUG": "False",
30 | "NPM_CONFIG_PRODUCTION": "true",
31 | "USE_S3": "True",
32 | "SET_HSTS": "True",
33 | "SSL_REDIRECT": "True",
34 | "X_FRAME_OPTIONS": "DENY",
35 | "XSS_PROTECTION": "True",
36 | "CSRF_COOKIE_SECURE": "True",
37 | "SESSION_COOKIE_SECURE": "True",
38 | "CSP_SCRIPT_SRC": "'self' 'unsafe-inline' 'unsafe-eval' www.google-analytics.com js.braintreegateway.com assets.braintreegateway.com www.paypalobjects.com *.paypal.com *.fundraiseup.com *.stripe.com m.stripe.network *.plaid.com *.src.mastercard.com *.checkout.visa.com pay.google.com",
39 | "CSP_IMG_SRC": "* data: *.fundraiseup.com ucarecdn.com pay.google.com",
40 | "CSP_FONT_SRC": "'self' fonts.googleapis.com fonts.gstatic.com *.fundraiseup.com *.stripe.com",
41 | "CSP_STYLE_SRC": "'self' 'unsafe-inline' fonts.googleapis.com fonts.gstatic.com",
42 | "CSP_FRAME_SRC": "'self' assets.braintreegateway.com *.paypal.com *.fundraiseup.com *.stripe.com *.plaid.com pay.google.com",
43 | "CSP_CONNECT_SRC": "'self' api.sandbox.braintreegateway.com client-analytics.sandbox.braintreegateway.com api.braintreegateway.com client-analytics.braintreegateway.com *.braintree-api.com *.paypal.com www.google-analytics.com https://www.mozilla.org/en-US/newsletter/ fndrsp.net fndrsp-checkout.net *.fundraiseup.com *.stripe.com *.plaid.com *.mastercard.com *.checkout.visa.com api.addressy.com"
44 | },
45 | "scripts": {
46 | "postdeploy": "python manage.py load_fake_data && python manage.py review_app_admin",
47 | "pr-predestroy": "python manage.py clear_fake_data"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/source/sass/components/_hero.scss:
--------------------------------------------------------------------------------
1 | .hero {
2 | &__container {
3 | position: relative;
4 | margin: 0 auto $gutter;
5 | border-bottom: 1px solid $color--border;
6 |
7 | @include media-query(tablet-portrait) {
8 | margin: 0 0 ($gutter * 1.5);
9 | max-height: 464px;
10 | border-bottom: 0;
11 | overflow: hidden;
12 | }
13 | }
14 |
15 | &__content {
16 | padding: ($gutter / 2) $gutter;
17 |
18 | @include media-query(tablet-portrait) {
19 | position: absolute;
20 | top: 0;
21 | width: 100%;
22 | height: 100%;
23 | padding: 0 ($gutter * 4);
24 | }
25 |
26 | @include media-query(desktop-wide) {
27 | padding: 0;
28 | }
29 | }
30 |
31 | &__content-container {
32 | @include media-query(tablet-portrait) {
33 | margin: 0 auto;
34 | display: flex;
35 | flex-direction: column;
36 | justify-content: center;
37 | height: 100%;
38 | }
39 | @include media-query(desktop) {
40 | max-width: $site-width--default;
41 | }
42 | }
43 |
44 | &__image {
45 | background-color: $color--black;
46 | margin-bottom: ($gutter / 2);
47 |
48 | @include media-query(tablet-portrait) {
49 | margin-bottom: 0;
50 | }
51 | }
52 |
53 | &__image-item {
54 | display: block;
55 | width: 100%;
56 | height: auto;
57 |
58 | @include media-query(tablet-portrait) {
59 | opacity: 0.8;
60 | }
61 | }
62 |
63 | &__heading {
64 | @include font-size(xxl);
65 | color: $color--black;
66 | font-weight: $weight--medium;
67 | margin-bottom: ($gutter / 2);
68 |
69 | @include media-query(tablet-portrait) {
70 | color: $color--white;
71 | margin-bottom: $gutter;
72 | max-width: 60%;
73 | }
74 |
75 | @include media-query(tablet-landscape) {
76 | @include rem-font-size(56px); // one off font size
77 | }
78 | }
79 |
80 | &__description {
81 | @include font-size(m);
82 | font-weight: $weight--light;
83 | color: $color--black;
84 |
85 | @include media-query(tablet-portrait) {
86 | color: $color--white;
87 | max-width: 90%;
88 | }
89 |
90 | @include media-query(desktop) {
91 | font-weight: $weight--bold;
92 | max-width: 60%;
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/donate/payments/sqs.py:
--------------------------------------------------------------------------------
1 | from functools import lru_cache
2 | import json
3 | import logging
4 | from time import time, sleep
5 | import sched
6 | from django.conf import settings
7 | from django.core.serializers.json import DjangoJSONEncoder
8 |
9 | import botocore
10 | import boto3
11 |
12 | logger = logging.getLogger(__name__)
13 | schedule = sched.scheduler(time, sleep)
14 |
15 |
16 | @lru_cache(maxsize=1)
17 | def sqs_client():
18 | if all([settings.AWS_ACCESS_KEY_ID, settings.AWS_SECRET_ACCESS_KEY, settings.AWS_REGION]):
19 | return boto3.client(
20 | 'sqs',
21 | aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
22 | aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
23 | region_name=settings.AWS_REGION
24 | )
25 |
26 |
27 | def attempt_send_to_sqs(client, payload):
28 | client.send_message(
29 | QueueUrl=settings.BASKET_SQS_QUEUE_URL,
30 | MessageBody=json.dumps(payload, cls=DjangoJSONEncoder, sort_keys=True),
31 | )
32 |
33 |
34 | def send_to_sqs(payload):
35 | # If BASKET_SQS_QUEUE_URL is not configured, do nothing (djangorq is logging the payload).
36 | if settings.BASKET_SQS_QUEUE_URL:
37 | client = sqs_client()
38 | if client is None:
39 | logger.error("Could not connect to SQS Client.")
40 | return
41 |
42 | send_retries = 3
43 | send_data_immediately = 0 # seconds
44 | send_data_delay = 10 # seconds
45 | schedule_priority = 1
46 | call_args = (client, payload)
47 |
48 | for attempt in range(send_retries):
49 | if attempt == 0:
50 | schedule.enter(send_data_immediately, schedule_priority, attempt_send_to_sqs, call_args)
51 | else:
52 | schedule.enter(send_data_delay, schedule_priority, attempt_send_to_sqs, call_args)
53 | try:
54 | # explicitly block, just in case the implicit behaviour changes in the future
55 | schedule.run(blocking=True)
56 | break
57 | except botocore.exceptions.ClientError as err:
58 | if attempt != 2:
59 | logger.error(f"Error when sending data to SQS: {err}")
60 | else:
61 | logger.error(f"Could not send data to SQS. Unable to connect after {send_retries} retries.")
62 |
63 | continue
64 |
--------------------------------------------------------------------------------
/donate/templates/payment/newsletter_signup_master.html:
--------------------------------------------------------------------------------
1 | {% extends "pages/base_page.html" %}
2 | {% load i18n form_tags %}
3 |
4 | {% block title %}{% trans "Stay in touch" %}{% endblock %}
5 |
6 | {% block template_name %}app--newsletter-signup{% endblock %}
7 |
8 | {% block content %}
9 |
10 |
11 |
12 | {% block thank-you %}
13 |
{% trans "Thank you for your generous gift" %}
14 |
{% blocktrans trimmed %}
15 | Protect the internet as a global public resource. Join our email list to take action and stay updated!
16 | {% endblocktrans %}
17 | {% endblock %}
18 |
19 |
46 |
47 |
48 |
49 | {% endblock %}
50 |
--------------------------------------------------------------------------------
/donate/settings/languages.py:
--------------------------------------------------------------------------------
1 | LANGUAGES = [
2 | ('ach', 'Acholi'),
3 | ('ar', 'Arabic'),
4 | ('ast', 'Asturian'),
5 | ('az', 'Azerbaijani'),
6 | ('bn', 'Bengali'),
7 | ('bs', 'Bosnian'),
8 | ('ca', 'Catalan'),
9 | ('cs', 'Czech'),
10 | ('cy', 'Welsh'),
11 | ('da', 'Danish'),
12 | ('de', 'German'),
13 | ('dsb', 'Sorbian, Lower'),
14 | ('el', 'Greek'),
15 | ('en-US', 'English (US)'),
16 | ('en-AU', 'English (Australia)'),
17 | ('en-CA', 'English (Canada)'),
18 | ('en-GB', 'English (Great Britain)'),
19 | ('en-IN', 'English (India)'),
20 | ('en-NZ', 'English (New Zealand)'),
21 | ('es', 'Spanish'),
22 | ('es-MX', 'Spanish (Mexico)'),
23 | ('es-XL', 'Spanish (Latin America)'),
24 | ('et', 'Estonian'),
25 | ('fr', 'French'),
26 | ('fy-NL', 'Frisian'),
27 | ('gu-IN', 'Gujarati'),
28 | ('he', 'Hebrew'),
29 | ('hi-IN', 'Hindi'),
30 | ('hr', 'Croatian'),
31 | ('hsb', 'Sorbian, Upper'),
32 | ('hu', 'Hungarian'),
33 | ('ia', 'Interlingua'),
34 | ('id', 'Indonesian'),
35 | ('it', 'Italian'),
36 | ('ja', 'Japanese'),
37 | ('ka', 'Georgian'),
38 | ('kab', 'Kabyle'),
39 | ('ko', 'Korean'),
40 | ('lg', 'Luganda'),
41 | ('lo', 'Lao'),
42 | ('lv', 'Latvian'),
43 | ('ml', 'Malayalam'),
44 | ('mr', 'Marathi'),
45 | ('nb-NO', 'Norwegian Bokmål'),
46 | ('nl', 'Dutch'),
47 | ('nn-NO', 'Norwegian Nynorsk'),
48 | ('pl', 'Polish'),
49 | ('pt-BR', 'Portuguese (Brazil)'),
50 | ('pt-PT', 'Portuguese (Portugal)'),
51 | ('pa-IN', 'Punjabi'),
52 | ('ro', 'Romanian'),
53 | ('ru', 'Russian'),
54 | ('sk', 'Slovak'),
55 | ('sl', 'Slovenian'),
56 | ('sq', 'Albanian'),
57 | ('sv-SE', 'Swedish'),
58 | ('ta', 'Tamil'),
59 | ('te', 'Telugu'),
60 | ('th', 'Thai'),
61 | ('tr', 'Turkish'),
62 | ('uk', 'Ukrainian'),
63 | ('uz', 'Uzbek'),
64 | ('vi', 'Vietnamese'),
65 | ('zh-CN', 'Chinese (China)'),
66 | ('zh-TW', 'Chinese (Taiwan)'),
67 | ]
68 |
69 | # Collection of different language email templates,
70 | # for receipt sending through acoustic in payments/tasks.py.
71 | LANGUAGE_IDS = {
72 | "ja": "2960734",
73 | "pl": "2959660",
74 | "pt-BR": "2960746",
75 | "cs": "2960691",
76 | "de": "2960705",
77 | "es": "2960719",
78 | "fr": "2960725",
79 | "en-US": "2959686",
80 | }
81 |
--------------------------------------------------------------------------------
/docs/pontoon_integration.md:
--------------------------------------------------------------------------------
1 | # Pontoon integration
2 |
3 | [Pontoon](https://github.com/mozilla/pontoon) is Mozilla's Localization Platform. We use it to translate our donation platform (prod only).
4 |
5 | ## Configuration
6 |
7 | ### Settings
8 |
9 | **SSH Configuration:**
10 |
11 | Django based strings are translated on Pontoon, pushed directly to the `donate-wagtail` repo, and deployed with the rest of the code. For Wagtail based strings, we need an intermediate repo, [mozilla-donate-content](https://github.com/mozilla-l10n/mozilla-donate-content), that gets the latest translations from Pontoon and the newest strings to translate from `donate-wagtail`. To push to that repo, we need to add a deploy key with write access on GitHub. On Heroku, we need to run a script (`.profile`) that reads the `SSH_KEY` and `SSH_CONFIG` from the environment variables and write them as files. This script runs every time a dyno is created.
12 |
13 | **Pontoon sync:**
14 |
15 | A scheduled task runs every 20 minutes to sync the donate platform with the `mozilla-donate-content` repo. Users can trigger a sync in the Pontoon settings of the CMS admin.
16 |
17 | **Environment Variables:**
18 |
19 | - `USE_PONTOON`: set at `True` to use Pontoon. If set to `False`, the SSH configuration script won't run.
20 | - `SSH_KEY`: SSH key to be able to push to `mozilla-donate-content`.
21 | - `SSH_CONFIG`: must be set to `StrictHostKeyChecking=no`.
22 |
23 | **Heroku requirements:**
24 |
25 | - A worker dyno running `python manage.py rqworker wagtail_localize_pontoon.sync`,
26 | - `Heroku Redis` add-on,
27 | - `Heroku scheduler` running `python manage.py enqueue_pontoon_sync` every 20 min.
28 |
29 | ### First run
30 |
31 | The initial sync needs to be run manually:
32 |
33 | - **If the content repo is empty:** `touch README.md` and push this file to the main branch of the content repo.
34 | - run `heroku login`.
35 | - run `heroku run bash -a donate-wagtail-production`.
36 | - run `python manage.py sync_languages`: creates `Language` objects for all languages defined in the `LANGUAGES` section of `settings.py`.
37 | - run `python manage.py submit_whole_site_to_pontoon`: generates submissions for all live translatable pages.
38 | - run `python manage.py sync_pontoon`: pushes the source strings to `mozilla-donate-content`.
39 |
40 | ## Thunderbird donate website
41 |
42 | Thunderbird donate stack is also using Pontoon. The configuration is the same as Mozilla donate, the only difference being the content repo: [thunderbird-donate-content](https://github.com/mozilla-l10n/thunderbird-donate-content).
43 |
--------------------------------------------------------------------------------
/source/js/components/post-code-validation.js:
--------------------------------------------------------------------------------
1 | // Importing JSON list of countries and post-code info.
2 | // This info is also shared with donate/forms.py to check on the backend.
3 | import countriesAndPostCodes from "./post-codes-list.json";
4 |
5 | function enableCountryPostCodeValidation() {
6 | const countrySelector = document.getElementById("id_country");
7 |
8 | if (countrySelector && countriesAndPostCodes) {
9 | const postCodeContainer = document.querySelector(".post-code-container");
10 | const postCodeInput = document.getElementById("id_post_code");
11 | const formAndCityContainer = document.querySelector(
12 | ".form__group--city-post"
13 | );
14 |
15 | // runs once as part of our initialisation:
16 | checkForCountryPostCode(
17 | countrySelector,
18 | postCodeContainer,
19 | postCodeInput,
20 | formAndCityContainer
21 | );
22 |
23 | // add make sure this also runs every time the country selector gets changed:
24 | countrySelector.addEventListener("change", (e) => {
25 | checkForCountryPostCode(
26 | countrySelector,
27 | postCodeContainer,
28 | postCodeInput,
29 | formAndCityContainer
30 | );
31 | });
32 | }
33 | }
34 |
35 | function checkForCountryPostCode(
36 | countrySelector,
37 | postCodeContainer,
38 | postCodeInput,
39 | formAndCityContainer
40 | ) {
41 | const { options, selectedIndex } = countrySelector;
42 | const selectedCountryName = options[selectedIndex].text;
43 | // Finding the country object in the reference array.
44 | const countryObject = countriesAndPostCodes.find(
45 | (country) => country.name === selectedCountryName
46 | );
47 |
48 | if (countryObject !== undefined) {
49 | if (countryObject.postal) {
50 | // Display post code field.
51 | formAndCityContainer.style.setProperty(
52 | "grid-template-columns",
53 | "0.3fr 0.7fr"
54 | );
55 | postCodeContainer.classList.remove("hidden");
56 | }
57 | // Hide post code field.
58 | else {
59 | formAndCityContainer.style.setProperty("grid-template-columns", "auto");
60 | postCodeContainer.classList.add("hidden");
61 | postCodeInput.value = "";
62 | }
63 | } else {
64 | // (Default Case) If country not found, display post code field.
65 | formAndCityContainer.style.setProperty(
66 | "grid-template-columns",
67 | "0.3fr 0.7fr"
68 | );
69 | postCodeContainer.classList.remove("hidden");
70 | }
71 | }
72 |
73 | export default enableCountryPostCodeValidation;
74 |
--------------------------------------------------------------------------------
/donate/templates/pages/core/landing_page.html:
--------------------------------------------------------------------------------
1 | {% extends "pages/core/landing_page_master.html" %}
2 | {% load static wagtailcore_tags wagtailimages_tags i18n %}
3 |
4 | {% block content %}
5 |
6 |
7 |
8 |
9 |
10 | {% block featured_image %}
11 |
12 |
13 | {% image page.featured_image fill-290x95 as imageMobile %}
14 | {% image page.featured_image fill-580x190 as imageMobile_2x %}
15 | {% image page.featured_image width-350 as imageTablet %}
16 | {% image page.featured_image width-700 as imageTablet_2x %}
17 | {% image page.featured_image width-690 as imageDesktop %}
18 | {% image page.featured_image width-1380 as imageDesktop_2x %}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {% endblock %}
28 |
29 |
30 |
{{ page.title }}
31 |
32 | {{ page.intro|richtext }}
33 |
34 |
35 |
36 |
37 |
38 |
41 | {% include "fragments/donate_form_fundraise_up.html" %}
42 |
43 |
44 |
45 | {% include "fragments/donate_form_disclaimer_fundraise_up.html" %}
46 |
47 |
48 |
49 |
50 | {% endblock %}
51 |
--------------------------------------------------------------------------------
/donate/core/templatetags/util_tags.py:
--------------------------------------------------------------------------------
1 | import locale
2 | import unicodedata
3 | from decimal import Decimal
4 |
5 | from django import template
6 | from django.conf import settings
7 | from django.utils.translation import get_language_info, to_locale
8 |
9 | from babel.core import Locale
10 | from babel.numbers import (
11 | format_currency as babel_format_currency,
12 | get_currency_symbol
13 | )
14 |
15 | from ..constants import LOCALE_MAP
16 |
17 | register = template.Library()
18 |
19 |
20 | def to_known_locale(code):
21 | code = LOCALE_MAP.get(code, code)
22 | return to_locale(code)
23 |
24 |
25 | # Generates a sorted list of currently supported locales. For each locale, the list
26 | # contains the locale code and the local name of the locale.
27 | # To sort the list by local names, we use:
28 | # - Case folding, in order to do case-insensitive comparison, and more.
29 | # - String normalization using the Normalization Form Canonical Decomposition, to compare
30 | # canonical equivalence (e.g. without diacritics)
31 | @register.simple_tag()
32 | def get_local_language_names():
33 | locale.setlocale(locale.LC_ALL, "C.UTF-8")
34 | languages = []
35 | for lang in settings.LANGUAGES:
36 | languages.append([lang[0], get_language_info(lang[0])['name_local']])
37 | return sorted(languages, key=lambda x: locale.strxfrm(unicodedata.normalize('NFD', x[1])).casefold())
38 |
39 |
40 | @register.simple_tag(takes_context=True)
41 | def get_locale(context):
42 | return to_known_locale(context['request'].LANGUAGE_CODE)
43 |
44 |
45 | @register.simple_tag()
46 | def format_currency(language_code, currency_code, amount):
47 | locale = to_known_locale(language_code)
48 | locale_obj = Locale.parse(locale)
49 | pattern = locale_obj.currency_formats['standard'].pattern
50 |
51 | # By default, Babel will display a fixed number of decimal places based on the
52 | # default format for the currency. It doesn't offer any way to tell
53 | # format_currency to hide decimals for integer values
54 | # see https://github.com/python-babel/babel/issues/478
55 | # In order to work around this, we fetch the pattern for the currency in
56 | # the current locale, and replace a padded decimal with an optional one.
57 | # We also have to set currency_digits=False otherwise this gets ignored entirely.
58 | if Decimal(amount) == int(float(amount)):
59 | pattern = pattern.replace('0.00', '0.##')
60 |
61 | return babel_format_currency(
62 | amount, currency_code.upper(), format=pattern, locale=locale_obj, currency_digits=False
63 | )
64 |
65 |
66 | @register.simple_tag(takes_context=True)
67 | def get_localized_currency_symbol(context, currency_code):
68 | locale = to_known_locale(context['request'].LANGUAGE_CODE)
69 | return get_currency_symbol(currency_code.upper(), locale)
70 |
--------------------------------------------------------------------------------
/env.default:
--------------------------------------------------------------------------------
1 | # Wagtail config
2 | DJANGO_CONFIGURATION=Development
3 | DJANGO_SETTINGS_MODULE=donate.settings
4 |
5 | ALLOWED_HOSTS=*
6 | CONTENT_TYPE_NO_SNIFF=True
7 | DATABASE_URL=postgres://donate:mozilla@postgres:5432/donate
8 | DEBUG=True
9 | VSCODE_DEBUGGER=False
10 | DJANGO_LOG_LEVEL=INFO
11 | DJANGO_SECRET_KEY=secret
12 | HEROKU_APP_NAME=
13 | REDIS_URL=redis://redis:6379/0
14 | SET_HSTS=False
15 | SSL_REDIRECT=False
16 | USE_X_FORWARDED_HOST=False
17 | X_FRAME_OPTIONS=DENY
18 | XSS_PROTECTION=True
19 | CSRF_COOKIE_SECURE=False
20 | SESSION_COOKIE_SECURE=False
21 |
22 | # S3
23 | USE_S3=False
24 | AWS_LOCATION=
25 | AWS_REGION=
26 | AWS_ACCESS_KEY_ID=
27 | AWS_S3_CUSTOM_DOMAIN=
28 | AWS_SECRET_ACCESS_KEY=
29 | AWS_STORAGE_BUCKET_NAME=
30 |
31 | # Apple Pay Domain Association Key (for FundraiseUp)
32 | APPLE_PAY_DOMAIN_ASSOCIATION_KEY=
33 |
34 | # Acoustic Config
35 | ACOUSTIC_TX_CLIENT_ID=
36 | ACOUSTIC_TX_CLIENT_SECRET=
37 | ACOUSTIC_TX_REFRESH_TOKEN=
38 | ACOUSTIC_TX_SERVER_NUMBER=
39 |
40 | # Desired method of sending donation receipt emails.
41 | # Can either be "BASKET" to let basket handle it,
42 | # or "DONATE" to send emails through donate stack.
43 | DONATION_RECEIPT_METHOD=BASKET
44 |
45 |
46 |
47 | # Braintree
48 | BRAINTREE_USE_SANDBOX=True
49 | BRAINTREE_MERCHANT_ID=test
50 | BRAINTREE_MERCHANT_ACCOUNTS=usd=usd-ac,gbp=gbp-ac
51 | BRAINTREE_MERCHANT_ACCOUNTS_PAYPAL_MICRO=
52 | BRAINTREE_PLANS=usd=usd-plan,gbp=gbp-plan
53 | BRAINTREE_PUBLIC_KEY=test
54 | BRAINTREE_PRIVATE_KEY=test
55 | BRAINTREE_TOKENIZATION_KEY=
56 |
57 | # Basket
58 | BASKET_API_ROOT_URL=
59 | BASKET_SQS_QUEUE_URL=
60 |
61 | # See the Basket client docs https://basket-client.readthedocs.io/en/latest/install.html
62 | BASKET_API_ROOT_URL=https://basket-dev.allizom.org/
63 |
64 | # CSP config
65 | # Default values for CSP items can be found in "donate/settings/defaults.py"
66 |
67 | # Paypal settings
68 | USE_PAYPAL=True
69 |
70 | # Recaptcha - these are test keys that work only on localhost
71 | USE_RECAPTCHA=False
72 | USE_CHECKBOX_RECAPTCHA_FOR_CC=False
73 |
74 | RECAPTCHA_SITE_KEY=
75 | RECAPTCHA_SECRET_KEY=
76 |
77 | RECAPTCHA_SITE_KEY_CHECKBOX=
78 | RECAPTCHA_SECRET_KEY_CHECKBOX=
79 |
80 | RECAPTCHA_SITE_KEY_REGULAR=
81 |
82 | # Stripe
83 | STRIPE_API_KEY=
84 | STRIPE_WEBHOOK_SECRET=
85 |
86 | # Sentry
87 | SENTRY_DSN=
88 | SENTRY_ENVIRONMENT=
89 |
90 | # OIDC
91 | USE_CONVENTIONAL_AUTH=True
92 | OIDC_RP_CLIENT_ID=YourClientIdHere
93 | OIDC_RP_CLIENT_SECRET=YourClientSecretHere
94 |
95 | # Salesforce
96 | SALESFORCE_ORGID=
97 |
98 | # Thunderbird Redirect
99 | ENABLE_THUNDERBIRD_REDIRECT=False
100 |
101 | # Translations
102 | LOCAL_PATH_TO_L10N_REPO=
103 |
104 | # Thunderbird Mailchimp settings
105 | THUNDERBIRD_MC_API_KEY=
106 | THUNDERBIRD_MC_SERVER=
107 | THUNDERBIRD_MC_LIST_ID=
108 |
--------------------------------------------------------------------------------
/donate/thunderbird/templates/payment/thank_you.html:
--------------------------------------------------------------------------------
1 | {% extends "payment/thank_you_master.html" %}
2 |
3 | {% load i18n util_tags %}
4 |
5 | {% block thank_you_description %}
6 | {% blocktrans with url='/help' %}We’ve emailed you a donation receipt; if it’s missing, please check your junk/spam folders, then contact us using this form .{% endblocktrans %}
7 | {% trans "Lastly, can you multiply your impact by sharing about the important work Thunderbird is doing? Thank you again!" %}
8 | {% endblock %}
9 |
10 | {% block buttons %}
11 | {% trans "I donated to @mozthunderbird today to #freetheinbox. Join me to support communication privacy." as twitter_text context "Used as a tweet" %}
12 | {% trans "I donated to Thunderbird today" as email_subject context "Email subject line" %}
13 | {% with page_link="https://give.thunderbird.net" %}
14 |
15 |
16 |
17 |
18 |
19 | {% trans "Facebook" %}
20 |
21 | {# see https://dev.twitter.com/web/tweet-button/web-intent #}
22 |
23 |
26 | {% trans "Twitter" %}
27 |
28 |
29 |
30 |
31 |
32 | {% trans "Email" context "Share button" %}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | {% trans "Link" context "Share button" %}
43 | {% trans "Copied" %}
44 |
45 |
46 |
47 | {% endwith %}
48 | {% endblock %}
49 |
--------------------------------------------------------------------------------
/donate/templates/pages/core/campaign_page.html:
--------------------------------------------------------------------------------
1 | {% extends "pages/core/campaign_page_master.html" %}
2 | {% load static wagtailcore_tags wagtailimages_tags i18n %}
3 |
4 | {% block content %}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | {% with alt_text=image.alt %}
14 | {% image page.hero_image width-1800 as imageDesktop %}
15 | {% image page.hero_image width-3600 as imageDesktop_2x %}
16 | {% image page.hero_image width-1200 as imageTablet %}
17 | {% image page.hero_image width-2400 as imageTablet_2x %}
18 | {% image page.hero_image width-768 as imageMobile %}
19 | {% image page.hero_image width-1536 as imageMobile_2x %}
20 |
21 |
22 |
23 |
24 |
25 | {% endwith %}
26 |
27 |
28 |
29 |
30 |
{{ page.title }}
31 |
{{ page.lead_text }}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | {% if page.cta_first == True %}
40 | {% include "fragments/campaign_page_fundraiseup_form.html" with order="order__2" %}
41 | {% include "fragments/campaign_page_intro.html" with order="order__1" %}
42 | {% else %}
43 | {% include "fragments/campaign_page_intro.html" with order="order__1" %}
44 | {% include "fragments/campaign_page_fundraiseup_form.html" with order="order__2" %}
45 | {% endif %}
46 |
47 | {% include "fragments/donate_form_disclaimer_fundraise_up.html" %}
48 |
49 |
50 |
51 |
52 | {% endblock %}
53 |
--------------------------------------------------------------------------------
/donate/templates/fragments/donate_form_disclaimer_master.html:
--------------------------------------------------------------------------------
1 | {% load i18n util_tags %}
2 |
3 |
49 |
--------------------------------------------------------------------------------