diff --git a/.profile b/.profile new file mode 100644 index 00000000000..f591ca7f872 --- /dev/null +++ b/.profile @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +# Copy SSH private key to file, if set +# This is used for talking to GitHub over an SSH connection +if [ $WAGTAIL_LOCALIZE_PRIVATE_KEY ]; then + echo "Generating SSH config" + SSH_DIR=/app/.ssh + + mkdir -p $SSH_DIR + chmod 700 $SSH_DIR + + echo $WAGTAIL_LOCALIZE_PRIVATE_KEY | base64 --decode > $SSH_DIR/id_rsa + + chmod 400 $SSH_DIR/id_rsa + + cat << EOF > $SSH_DIR/config +StrictHostKeyChecking no +EOF + + chmod 600 $SSH_DIR/config + + echo "Done!" +fi diff --git a/dev-requirements.txt b/dev-requirements.txt index 3221abefec9..cebcd4db766 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -4,16 +4,16 @@ # # pip-compile dev-requirements.in # -certifi==2021.5.30 +certifi==2020.4.5.1 # via # -c requirements.txt # requests # urllib3 -cffi==1.14.6 +cffi==1.14.0 # via # -c requirements.txt # cryptography -charset-normalizer==2.0.3 +chardet==3.0.4 # via # -c requirements.txt # requests @@ -21,7 +21,7 @@ coverage==5.1 # via coveralls coveralls==3.0.1 # via -r dev-requirements.in -cryptography==3.4.7 +cryptography==3.2 # via # -c requirements.txt # pyopenssl @@ -30,12 +30,12 @@ docopt==0.6.2 # via coveralls flake8==3.8.4 # via -r dev-requirements.in -idna==3.2 +idna==2.9 # via # -c requirements.txt # requests # urllib3 -importlib-metadata==4.6.1 +importlib-metadata==3.10.1 # via # -c requirements.txt # flake8 @@ -55,23 +55,24 @@ pyopenssl==20.0.1 # via # -c requirements.txt # urllib3 -requests==2.26.0 +requests==2.25.1 # via # -c requirements.txt # coveralls -six==1.16.0 +six==1.14.0 # via # -c requirements.txt + # cryptography # pyopenssl -typing-extensions==3.10.0.0 +typing-extensions==3.7.4.3 # via # -c requirements.txt # importlib-metadata -urllib3[secure]==1.26.6 +urllib3[secure]==1.25.9 # via # -c requirements.txt # requests -zipp==3.5.0 +zipp==3.4.1 # via # -c requirements.txt # importlib-metadata diff --git a/network-api/networkapi/campaign/views.py b/network-api/networkapi/campaign/views.py index 071f39b31f4..f722d091393 100644 --- a/network-api/networkapi/campaign/views.py +++ b/network-api/networkapi/campaign/views.py @@ -13,6 +13,14 @@ from networkapi.wagtailpages.models import Petition, Signup +def process_lang_code(lang): + # Salesforce expects "pt" instead of "pt-BR". + # See https://github.com/mozilla/foundation.mozilla.org/issues/5993 + if lang == 'pt-BR': + return 'pt' + return lang + + class SQSProxy: """ We use a proxy class to make sure that code that @@ -128,7 +136,7 @@ def signup_submission(request, signup): "format": "html", "source_url": source, "newsletters": signup.newsletter, - "lang": rq.get('lang', 'en'), + "lang": process_lang_code(rq.get('lang', 'en')), "country": rq.get('country', ''), # Empty string instead of None due to Basket issues "first_name": rq.get('givenNames', ''), @@ -178,7 +186,7 @@ def petition_submission(request, petition): "email": request.data['email'], "email_subscription": request.data['newsletterSignup'], "source_url": request.data['source'], - "lang": request.data['lang'], + "lang": process_lang_code(request.data['lang']), } if petition: diff --git a/network-api/networkapi/highlights/migrations/0005_auto_20210531_1735.py b/network-api/networkapi/highlights/migrations/0005_auto_20210531_1735.py new file mode 100644 index 00000000000..6324c50e00a --- /dev/null +++ b/network-api/networkapi/highlights/migrations/0005_auto_20210531_1735.py @@ -0,0 +1,25 @@ +# Generated by Django 3.1.11 on 2021-05-31 17:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('wagtailcore', '0062_comment_models_and_pagesubscription'), + ('highlights', '0004_remove_highlight_image'), + ] + + operations = [ + migrations.AddField( + model_name='highlight', + name='locale', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='wagtailcore.locale'), + ), + migrations.AddField( + model_name='highlight', + name='translation_key', + field=models.UUIDField(editable=False, null=True), + ), + ] diff --git a/network-api/networkapi/highlights/migrations/0006_bootstrap_migration.py b/network-api/networkapi/highlights/migrations/0006_bootstrap_migration.py new file mode 100644 index 00000000000..d318026ef75 --- /dev/null +++ b/network-api/networkapi/highlights/migrations/0006_bootstrap_migration.py @@ -0,0 +1,15 @@ +# Generated by Django 3.1.11 on 2021-05-31 17:18 + +from django.db import migrations +from wagtail.core.models import BootstrapTranslatableModel + + +class Migration(migrations.Migration): + + dependencies = [ + ('highlights', '0005_auto_20210531_1735'), + ] + + operations = [ + BootstrapTranslatableModel('highlights.Highlight'), + ] diff --git a/network-api/networkapi/highlights/migrations/0007_localize_migration.py b/network-api/networkapi/highlights/migrations/0007_localize_migration.py new file mode 100644 index 00000000000..b524774f1c5 --- /dev/null +++ b/network-api/networkapi/highlights/migrations/0007_localize_migration.py @@ -0,0 +1,31 @@ +# Generated by Django 3.1.11 on 2021-05-31 18:02 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('wagtailcore', '0062_comment_models_and_pagesubscription'), + ('highlights', '0006_bootstrap_migration'), + ] + + operations = [ + migrations.AlterField( + model_name='highlight', + name='locale', + field=models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='wagtailcore.locale'), + preserve_default=False, + ), + migrations.AlterField( + model_name='highlight', + name='translation_key', + field=models.UUIDField(default=uuid.uuid4, editable=False), + ), + migrations.AlterUniqueTogether( + name='highlight', + unique_together={('translation_key', 'locale')}, + ), + ] diff --git a/network-api/networkapi/highlights/models.py b/network-api/networkapi/highlights/models.py index ca7b56785aa..5e48b0a11e5 100644 --- a/network-api/networkapi/highlights/models.py +++ b/network-api/networkapi/highlights/models.py @@ -5,9 +5,12 @@ from adminsortable.models import SortableMixin from wagtail.admin.edit_handlers import FieldPanel from wagtail.core.fields import RichTextField +from wagtail.core.models import TranslatableMixin from wagtail.images.edit_handlers import ImageChooserPanel from wagtail.snippets.models import register_snippet +from wagtail_localize.fields import TranslatableField + from networkapi.utility.images import get_image_upload_path @@ -33,7 +36,7 @@ def published(self): @register_snippet -class Highlight(SortableMixin): +class Highlight(TranslatableMixin, SortableMixin): """ An data type to highlight things like pulse projects, custom pages, etc @@ -97,9 +100,16 @@ class Highlight(SortableMixin): FieldPanel("expires"), ] + translatable_fields = [ + TranslatableField('title'), + TranslatableField('description'), + TranslatableField('link_label'), + TranslatableField('footer'), + ] + objects = HighlightQuerySet.as_manager() - class Meta: + class Meta(TranslatableMixin.Meta): verbose_name_plural = 'highlights' ordering = ('order',) diff --git a/network-api/networkapi/management/commands/create_locales.py b/network-api/networkapi/management/commands/create_locales.py new file mode 100644 index 00000000000..ed7b0fce01e --- /dev/null +++ b/network-api/networkapi/management/commands/create_locales.py @@ -0,0 +1,13 @@ +from django.conf import settings +from django.core.management.base import BaseCommand +from wagtail.core.models import Locale + + +class Command(BaseCommand): + help = 'Look for and create locales if they do not exist. This can be run multiple times if needed.' + + def handle(self, *args, **options): + for language_code, name in settings.WAGTAIL_CONTENT_LANGUAGES: + locale, created = Locale.objects.get_or_create(language_code=language_code) + if created: + print(f"Create new locale: {name}") diff --git a/network-api/networkapi/management/commands/sync_locale.py b/network-api/networkapi/management/commands/sync_locale.py new file mode 100644 index 00000000000..18f67347dd7 --- /dev/null +++ b/network-api/networkapi/management/commands/sync_locale.py @@ -0,0 +1,38 @@ +from django.conf import settings +from django.core.management.base import BaseCommand +from wagtail.core.models import Locale +from wagtail_localize.models import LocaleSynchronization + + +class Command(BaseCommand): + help = 'Sync pages with original English pages' + + def handle(self, *args, **options): + print("Select a language code to sync with English. ie: de") + + for language_code, name in settings.WAGTAIL_CONTENT_LANGUAGES: + if language_code != 'en': + print(f"{language_code} ({name})") + + language_code = input("Language code: ") + + # Confirm the language code is in the WAGTAIL_CONTENT_LANGUAGES + language_codes_dict = dict(settings.WAGTAIL_CONTENT_LANGUAGES) + if language_code not in language_codes_dict: + print("Invalid language code") + return + + print("Getting both locales...") + english_locale, _ = Locale.objects.get_or_create(language_code='en') + locale, _ = Locale.objects.get_or_create(language_code=language_code) + + print("Getting LocaleSynchronization object") + sync, created = LocaleSynchronization.objects.get_or_create( + locale=locale, + sync_from=english_locale, + ) + if created: + print("\tNew LocaleSynchronization object created") + + print(f"Syncing {locale} from {english_locale}") + sync.sync_trees() diff --git a/network-api/networkapi/mozfest/migrations/0012_auto_20210525_1516.py b/network-api/networkapi/mozfest/migrations/0014_auto_20210525_1516.py similarity index 99% rename from network-api/networkapi/mozfest/migrations/0012_auto_20210525_1516.py rename to network-api/networkapi/mozfest/migrations/0014_auto_20210525_1516.py index 043af27a1cd..ef94043f1ee 100644 --- a/network-api/networkapi/mozfest/migrations/0012_auto_20210525_1516.py +++ b/network-api/networkapi/mozfest/migrations/0014_auto_20210525_1516.py @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ - ('mozfest', '0011_auto_20210519_1654'), + ('mozfest', '0013_auto_20210721_1921'), ] operations = [ diff --git a/network-api/networkapi/mozfest/models.py b/network-api/networkapi/mozfest/models.py index 9b266ee625c..32db42c3d84 100644 --- a/network-api/networkapi/mozfest/models.py +++ b/network-api/networkapi/mozfest/models.py @@ -4,6 +4,8 @@ from wagtail.core.models import Page from wagtail.images.edit_handlers import ImageChooserPanel from wagtail.snippets.edit_handlers import SnippetChooserPanel +from wagtail_localize.fields import SynchronizedField, TranslatableField + from networkapi.wagtailpages.utils import ( set_main_site_nav_information, @@ -167,10 +169,32 @@ class MozfestHomepage(MozfestPrimaryPage): else: content_panels = all_panels - # Because we inherit from PrimaryPage, but the "use_wide_templatae" property does nothing + # Because we inherit from PrimaryPage, but the "use_wide_template" property does nothing # we should hide it and make sure we use the right template settings_panels = Page.settings_panels + translatable_fields = [ + # Promote tab fields + SynchronizedField('slug'), + TranslatableField('seo_title'), + SynchronizedField('show_in_menus'), + TranslatableField('search_description'), + SynchronizedField('search_image'), + # Content tab fields + TranslatableField('title'), + TranslatableField('cta_button_label'), + SynchronizedField('cta_button_destination'), + TranslatableField('banner_heading'), + TranslatableField('banner_guide_text'), + SynchronizedField('banner_video_url'), + TranslatableField('title'), + TranslatableField('search_description'), + TranslatableField('search_image'), + TranslatableField('signup'), + TranslatableField('body'), + TranslatableField('footnotes'), + ] + def get_context(self, request): context = super().get_context(request) context['banner_video_type'] = self.specific.banner_video_type diff --git a/network-api/networkapi/news/migrations/0005_auto_20210531_1735.py b/network-api/networkapi/news/migrations/0005_auto_20210531_1735.py new file mode 100644 index 00000000000..a1ac79bd0d3 --- /dev/null +++ b/network-api/networkapi/news/migrations/0005_auto_20210531_1735.py @@ -0,0 +1,25 @@ +# Generated by Django 3.1.11 on 2021-05-31 17:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('wagtailcore', '0062_comment_models_and_pagesubscription'), + ('news', '0004_remove_news_featured'), + ] + + operations = [ + migrations.AddField( + model_name='news', + name='locale', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='wagtailcore.locale'), + ), + migrations.AddField( + model_name='news', + name='translation_key', + field=models.UUIDField(editable=False, null=True), + ), + ] diff --git a/network-api/networkapi/news/migrations/0006_bootstrap_migration.py b/network-api/networkapi/news/migrations/0006_bootstrap_migration.py new file mode 100644 index 00000000000..ead586276cd --- /dev/null +++ b/network-api/networkapi/news/migrations/0006_bootstrap_migration.py @@ -0,0 +1,15 @@ +# Generated by Django 3.1.11 on 2021-05-31 17:37 + +from django.db import migrations +from wagtail.core.models import BootstrapTranslatableModel + + +class Migration(migrations.Migration): + + dependencies = [ + ('news', '0005_auto_20210531_1735'), + ] + + operations = [ + BootstrapTranslatableModel('news.News') + ] diff --git a/network-api/networkapi/news/migrations/0007_localize_migration.py b/network-api/networkapi/news/migrations/0007_localize_migration.py new file mode 100644 index 00000000000..b89dd06a17a --- /dev/null +++ b/network-api/networkapi/news/migrations/0007_localize_migration.py @@ -0,0 +1,31 @@ +# Generated by Django 3.1.11 on 2021-05-31 18:02 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('wagtailcore', '0062_comment_models_and_pagesubscription'), + ('news', '0006_bootstrap_migration'), + ] + + operations = [ + migrations.AlterField( + model_name='news', + name='locale', + field=models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='wagtailcore.locale'), + preserve_default=False, + ), + migrations.AlterField( + model_name='news', + name='translation_key', + field=models.UUIDField(default=uuid.uuid4, editable=False), + ), + migrations.AlterUniqueTogether( + name='news', + unique_together={('translation_key', 'locale')}, + ), + ] diff --git a/network-api/networkapi/news/models.py b/network-api/networkapi/news/models.py index 7faafd5e67a..8c93dae2ce5 100644 --- a/network-api/networkapi/news/models.py +++ b/network-api/networkapi/news/models.py @@ -4,6 +4,7 @@ from networkapi.utility.images import get_image_upload_path from wagtail.snippets.models import register_snippet +from wagtail.core.models import TranslatableMixin def get_thumbnail_upload_path(instance, filename): @@ -29,7 +30,7 @@ def published(self): @register_snippet -class News(models.Model): +class News(TranslatableMixin, models.Model): """ Medium blog posts, articles and other media """ @@ -88,7 +89,7 @@ class News(models.Model): objects = NewsQuerySet.as_manager() - class Meta: + class Meta(TranslatableMixin.Meta): """Meta settings for news model""" verbose_name = 'news article' diff --git a/network-api/networkapi/settings.py b/network-api/networkapi/settings.py index 393bfc117f6..1596d99081d 100644 --- a/network-api/networkapi/settings.py +++ b/network-api/networkapi/settings.py @@ -50,6 +50,7 @@ FEED_LIMIT=(int, 10), FILEBROWSER_DEBUG=(bool, False), FILEBROWSER_DIRECTORY=(str, ''), + FORCE_500_STACK_TRACES=(bool, False), FRONTEND_CACHE_CLOUDFLARE_BEARER_TOKEN=(str, ''), FRONTEND_CACHE_CLOUDFLARE_ZONEID=(str, ''), GITHUB_TOKEN=(str, ''), @@ -85,6 +86,9 @@ USE_S3=(bool, True), USE_X_FORWARDED_HOST=(bool, False), WAGTAILIMAGES_INDEX_PAGE_SIZE=(int, 60), + WAGTAILLOCALIZE_GIT_URL=(str, ''), + WAGTAILLOCALIZE_GIT_CLONE_DIR=(str, ''), + WAGTAIL_LOCALIZE_PRIVATE_KEY=(str, ''), WEB_MONETIZATION_POINTER=(str, ''), XROBOTSTAG_ENABLED=(bool, False), XSS_PROTECTION=bool, @@ -138,6 +142,9 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = FILEBROWSER_DEBUG = env('DEBUG') +# SECURITY WARNING: same as above! +FORCE_500_STACK_TRACES = env('FORCE_500_STACK_TRACES') + # whether or not to send the X-Robots-Tag header XROBOTSTAG_ENABLED = env('XROBOTSTAG_ENABLED') @@ -221,6 +228,13 @@ 'modelcluster', 'taggit', + # Base wagtail localization + 'wagtail_localize', + 'wagtail_localize.locales', + + # git integration for localization + 'wagtail_localize_git', + 'rest_framework', 'django_filters', 'gunicorn', @@ -325,6 +339,7 @@ 'blog_tags': 'networkapi.wagtailpages.templatetags.blog_tags', 'card_tags': 'networkapi.wagtailpages.templatetags.card_tags', 'class_tags': 'networkapi.wagtailpages.templatetags.class_tags', + 'debug_tags': 'networkapi.wagtailpages.templatetags.debug_tags', 'homepage_tags': 'networkapi.wagtailpages.templatetags.homepage_tags', 'localization': 'networkapi.wagtailpages.templatetags.localization', 'mini_site_tags': 'networkapi.wagtailpages.templatetags.mini_site_tags', @@ -423,10 +438,10 @@ # https://docs.djangoproject.com/en/1.10/topics/i18n/ LANGUAGE_CODE = 'en' -LANGUAGES = ( +WAGTAIL_CONTENT_LANGUAGES = LANGUAGES = ( ('en', gettext_lazy('English')), ('de', gettext_lazy('German')), - ('pt', gettext_lazy('Portuguese')), + ('pt-BR', gettext_lazy('Portuguese (Brazil)')), ('es', gettext_lazy('Spanish')), ('fr', gettext_lazy('French')), ('fy-NL', gettext_lazy('Frisian')), @@ -434,6 +449,10 @@ ('pl', gettext_lazy('Polish')), ) +WAGTAILLOCALIZE_GIT_URL = env('WAGTAILLOCALIZE_GIT_URL') +WAGTAILLOCALIZE_GIT_CLONE_DIR = env('WAGTAILLOCALIZE_GIT_CLONE_DIR') +WAGTAIL_LOCALIZE_PRIVATE_KEY = env('WAGTAIL_LOCALIZE_PRIVATE_KEY') + TIME_ZONE = 'UTC' USE_I18N = True USE_L10N = True @@ -467,6 +486,7 @@ WAGTAIL_SITE_NAME = 'Mozilla Foundation' WAGTAILIMAGES_INDEX_PAGE_SIZE = env('WAGTAILIMAGES_INDEX_PAGE_SIZE') WAGTAIL_USAGE_COUNT_ENABLED = True +WAGTAIL_I18N_ENABLED = True # Wagtail Frontend Cache Invalidator Settings @@ -482,7 +502,7 @@ # Rest Framework Settings REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly', + 'rest_framework.permissions.IsAuthenticatedOrReadOnly', ] } diff --git a/network-api/networkapi/sitemaps.py b/network-api/networkapi/sitemaps.py new file mode 100644 index 00000000000..eee00f7ea6d --- /dev/null +++ b/network-api/networkapi/sitemaps.py @@ -0,0 +1,23 @@ +# Solution came from Aleksi44 on Github: +# https://github.com/wagtail/wagtail/issues/6583#issuecomment-798960446 +from django.contrib.sitemaps import views as sitemap_views +from wagtail.contrib.sitemaps.sitemap_generator import Sitemap + + +class CustomSitemap(Sitemap): + + def items(self): + return ( + self.get_wagtail_site() + .root_page + .localized # This is missing from sitemap_generator + .get_descendants(inclusive=True) + .live() + .public() + .order_by('path') + .specific()) + + +def sitemap(request, **kwargs): + sitemaps = {'wagtail': CustomSitemap(request)} + return sitemap_views.sitemap(request, sitemaps, **kwargs) diff --git a/network-api/networkapi/templates/fragments/canonical_url.html b/network-api/networkapi/templates/fragments/canonical_url.html index 8aa9be44f9a..bc1c206fea6 100644 --- a/network-api/networkapi/templates/fragments/canonical_url.html +++ b/network-api/networkapi/templates/fragments/canonical_url.html @@ -28,7 +28,7 @@ {% elif CODE == 'pa-IN' %} - {% elif CODE == 'pt' %} + {% elif CODE == 'pt-BR' %} diff --git a/network-api/networkapi/templates/fragments/language_switcher.html b/network-api/networkapi/templates/fragments/language_switcher.html index 6bbe1fb130d..0aed1e0b841 100644 --- a/network-api/networkapi/templates/fragments/language_switcher.html +++ b/network-api/networkapi/templates/fragments/language_switcher.html @@ -1,15 +1,16 @@ -{% load i18n localization %} +{% load i18n localization wagtailcore_tags %} + +{% get_current_language as current_language %} +{% get_local_language_names as languages %}