Full Code of LibraryOfCongress/concordia for AI

main 80cc6e1b2573 cached
673 files
3.0 MB
832.6k tokens
2770 symbols
1 requests
Download .txt
Showing preview only (3,325K chars total). Download the full file or copy to clipboard to get everything.
Repository: LibraryOfCongress/concordia
Branch: main
Commit: 80cc6e1b2573
Files: 673
Total size: 3.0 MB

Directory structure:
gitextract_frv4ewgf/

├── .cfnlintrc.yaml
├── .dockerignore
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── black.yml
│       ├── build.yml
│       ├── codeql.yml
│       ├── db_ops.yml
│       ├── dev-main-deploy.yml
│       ├── feature-branch-deploy.yml
│       ├── pip-audit.yml
│       ├── prod-deploy.yml
│       ├── renew_coverage.yml
│       ├── stage-hotfix-rel-deploy.yml
│       ├── stage-image-refresh.yml
│       ├── stage-release-deploy.yml
│       ├── test-main-deploy.yml
│       └── test.yml
├── .gitignore
├── Dockerfile
├── LICENSE.md
├── Loadtesting.md
├── MANIFEST.in
├── Makefile
├── Pipfile
├── README.md
├── build_containers.sh
├── celerybeat/
│   ├── Dockerfile
│   └── entrypoint.sh
├── cloudformation/
│   ├── LICENSE
│   ├── NOTICE
│   ├── README.md
│   ├── add_cloudflare_ips_to_sgs.py
│   ├── create_secrets.sh
│   ├── featurebranch.yaml
│   ├── images/
│   │   └── architecture-overview.graffle/
│   │       └── data.plist
│   ├── infrastructure/
│   │   ├── bastion-hosts.yaml
│   │   ├── data-load.yaml
│   │   ├── elasticache-feature.yaml
│   │   ├── elasticache.yaml
│   │   ├── elasticsearch.yaml
│   │   ├── fargate-cluster.yaml
│   │   ├── fargate-featurebranch.yaml
│   │   ├── jenkins-server.yaml
│   │   ├── network-acl.yaml
│   │   ├── opensearch.yaml
│   │   ├── rds.yaml
│   │   ├── search-proxy-task.yaml
│   │   ├── security-groups.yaml
│   │   └── vpc.yaml
│   ├── master.yaml
│   ├── stack_drift.sh
│   ├── sync_templates.sh
│   └── tests/
│       └── validate-templates.sh
├── concordia/
│   ├── __init__.py
│   ├── admin/
│   │   ├── __init__.py
│   │   ├── actions.py
│   │   ├── filters.py
│   │   ├── forms.py
│   │   ├── utils.py
│   │   └── views.py
│   ├── admin_site.py
│   ├── api/
│   │   ├── __init__.py
│   │   └── schemas.py
│   ├── api_views.py
│   ├── apps.py
│   ├── asgi.py
│   ├── authentication_backends.py
│   ├── celery.py
│   ├── consumers.py
│   ├── context_processors.py
│   ├── contextmanagers.py
│   ├── converters.py
│   ├── decorators.py
│   ├── documents.py
│   ├── exceptions.py
│   ├── forms.py
│   ├── logging.py
│   ├── maintenance.py
│   ├── management/
│   │   ├── __init__.py
│   │   └── commands/
│   │       ├── __init__.py
│   │       ├── calculate_difficulty_values.py
│   │       ├── create_load_test_fixtures.py
│   │       ├── ensure_initial_site_configuration.py
│   │       ├── import_site_reports.py
│   │       ├── prepare_load_test_db.py
│   │       └── print_frontend_test_urls.py
│   ├── middleware.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   ├── 0001_squashed_0040_remove_campaign_is_active.py
│   │   ├── 0002_auto_20181004_1848.py
│   │   ├── 0003_auto_20181004_2103.py
│   │   ├── 0004_auto_20181010_1715.py
│   │   ├── 0005_campaign_short_description.py
│   │   ├── 0006_campaignresource.py
│   │   ├── 0007_thumbnail_images.py
│   │   ├── 0008_auto_20181015_1711.py
│   │   ├── 0009_project_description.py
│   │   ├── 0010_auto_20181021_1659.py
│   │   ├── 0010_auto_20181022_1530.py
│   │   ├── 0011_auto_20181022_1532.py
│   │   ├── 0012_merge_20181022_1554.py
│   │   ├── 0013_auto_20181031_1305.py
│   │   ├── 0014_auto_20181115_1411.py
│   │   ├── 0015_auto_20181115_1436.py
│   │   ├── 0016_auto_20181115_1803.py
│   │   ├── 0017_change_transcription_supersedes_related_name.py
│   │   ├── 0018_auto_20181128_1611.py
│   │   ├── 0018_simplepage.py
│   │   ├── 0019_merge_20181128_1715.py
│   │   ├── 0020_auto_20181128_1718.py
│   │   ├── 0021_sitereport.py
│   │   ├── 0022_auto_20181211_1310.py
│   │   ├── 0023_auto_20190130_1555.py
│   │   ├── 0024_add_site_report_ordering.py
│   │   ├── 0024_auto_20190211_1420.py
│   │   ├── 0025_auto_20190329_1705.py
│   │   ├── 0025_unicode_slugs.py
│   │   ├── 0026_update_published_field_definition.py
│   │   ├── 0027_merge_20190423_1657.py
│   │   ├── 0028_asset_year.py
│   │   ├── 0029_assettranscriptionreservation_reservation_token.py
│   │   ├── 0030_auto_20190503_1559.py
│   │   ├── 0031_auto_20190509_1142.py
│   │   ├── 0032_topic_ordering.py
│   │   ├── 0033_simple_content_blocks.py
│   │   ├── 0034_auto_20190627_1438.py
│   │   ├── 0035_auto_20190627_1455.py
│   │   ├── 0036_auto_20190703_1203.py
│   │   ├── 0037_carouselslide.py
│   │   ├── 0038_sitereport_topic.py
│   │   ├── 0039_auto_20200129_1536.py
│   │   ├── 0040_auto_20200130_1756.py
│   │   ├── 0041_auto_20200203_1351.py
│   │   ├── 0042_auto_20200316_1623.py
│   │   ├── 0043_auto_20200323_1729.py
│   │   ├── 0044_auto_20200323_1827.py
│   │   ├── 0045_auto_20200323_1832.py
│   │   ├── 0046_auto_20200323_1907.py
│   │   ├── 0047_auto_20200324_1103.py
│   │   ├── 0048_auto_20200324_1820.py
│   │   ├── 0049_auto_20200324_2004.py
│   │   ├── 0050_auto_20210920_1544.py
│   │   ├── 0051_asset_storage_image.py
│   │   ├── 0052_auto_20220531_1331.py
│   │   ├── 0053_banner.py
│   │   ├── 0054_banner_active.py
│   │   ├── 0055_campaign_status.py
│   │   ├── 0056_auto_20220922_1508.py
│   │   ├── 0057_resource_resource_type.py
│   │   ├── 0058_banner_slug.py
│   │   ├── 0059_resourcefile.py
│   │   ├── 0060_alter_resourcefile_resource.py
│   │   ├── 0061_auto_20230201_1453.py
│   │   ├── 0061_sitereport_registered_contributors.py
│   │   ├── 0062_resourcefile_updated_on.py
│   │   ├── 0062_userretiredcampaign.py
│   │   ├── 0063_banner_alert_status.py
│   │   ├── 0064_alter_banner_alert_status.py
│   │   ├── 0065_alter_userretiredcampaign_unique_together.py
│   │   ├── 0066_auto_20230217_1302.py
│   │   ├── 0066_campaignretirementprogress.py
│   │   ├── 0067_alter_campaignretirementprogress_campaign.py
│   │   ├── 0068_campaignretirementprogress_complete.py
│   │   ├── 0069_merge_20230224_1446.py
│   │   ├── 0070_alter_campaign_options.py
│   │   ├── 0071_auto_20230306_1456.py
│   │   ├── 0072_merge_20230313_1047.py
│   │   ├── 0073_auto_20230314_1327.py
│   │   ├── 0074_auto_20230314_1341.py
│   │   ├── 0075_auto_20230327_1333.py
│   │   ├── 0076_sitereport_report_name.py
│   │   ├── 0077_alter_sitereport_report_name.py
│   │   ├── 0078_alter_sitereport_report_name.py
│   │   ├── 0079_auto_20230601_1234.py
│   │   ├── 0080_auto_20230602_0920.py
│   │   ├── 0081_sitereport_review_actions.py
│   │   ├── 0082_delete_userretiredcampaign.py
│   │   ├── 0083_sitereport_daily_active_users.py
│   │   ├── 0084_rename_review_actions_sitereport_daily_review_actions.py
│   │   ├── 0085_auto_20231016_1432.py
│   │   ├── 0086_auto_20231215_1311.py
│   │   ├── 0087_auto_20240213_0756.py
│   │   ├── 0088_alter_simplepage_body.py
│   │   ├── 0089_campaign_image_alt_text.py
│   │   ├── 0090_auto_20240408_1334.py
│   │   ├── 0091_guide_simple_page.py
│   │   ├── 0092_auto_20240509_1522.py
│   │   ├── 0093_asset_campaign.py
│   │   ├── 0094_alter_asset_campaign.py
│   │   ├── 0095_transcription_rolled_back_and_more.py
│   │   ├── 0096_transcription_source.py
│   │   ├── 0097_alter_sitereport_options_userprofile_review_count_and_more.py
│   │   ├── 0098_userprofile_create_and_population.py
│   │   ├── 0099_alter_campaign_display_on_homepage_and_more.py
│   │   ├── 0100_researchcenter.py
│   │   ├── 0101_auto_20241119_1215.py
│   │   ├── 0102_campaign_research_centers.py
│   │   ├── 0103_alter_item_title.py
│   │   ├── 0104_nexttranscribabletopicasset_and_more.py
│   │   ├── 0105_nextreviewablecampaignasset_concordia_n_transcr_aafdba_gin_and_more.py
│   │   ├── 0106_alter_nextreviewablecampaignasset_options_and_more.py
│   │   ├── 0107_alter_nextreviewablecampaignasset_options_and_more.py
│   │   ├── 0108_add_next_asset_cache_periodic_task.py
│   │   ├── 0109_alter_nextreviewablecampaignasset_asset_and_more.py
│   │   ├── 0110_remove_asset_media_url_alter_asset_storage_image.py
│   │   ├── 0111_auto_20250428_1023.py
│   │   ├── 0112_projecttopic_url_filter_alter_projecttopic_id.py
│   │   ├── 0113_create_asset_status_periodic_task.py
│   │   ├── 0114_create_daily_activity_periodic_task.py
│   │   ├── 0115_alter_asset_storage_image_alter_banner_link_and_more.py
│   │   ├── 0116_item_thumbnail_image.py
│   │   ├── 0117_alter_projecttopic_options_projecttopic_ordering.py
│   │   ├── 0118_asset_concordia_a_item_id_f10916_idx_and_more.py
│   │   ├── 0119_remove_asset_concordia_a_id_137ca8_idx_and_more.py
│   │   ├── 0120_sitereport_assets_started.py
│   │   ├── 0121_keymetricsreport.py
│   │   ├── 0122_alter_item_title.py
│   │   ├── 0123_alter_campaignretirementprogress_options.py
│   │   ├── 0124_update_periodic_task_paths.py
│   │   ├── 0125_update_userprofile_tasks.py
│   │   ├── 0126_concordiafile_helpfullink_remove_resource_campaign_and_more.py
│   │   ├── 0127_alter_campaignretirementprogress_options_and_more.py
│   │   ├── 0128_alter_campaignretirementprogress_options.py
│   │   └── __init__.py
│   ├── models.py
│   ├── parser.py
│   ├── passwords/
│   │   ├── LICENSE
│   │   ├── __init__.py
│   │   └── validators.py
│   ├── routing.py
│   ├── secrets.py
│   ├── settings_dev.py
│   ├── settings_docker.py
│   ├── settings_ecs.py
│   ├── settings_loadtest.py
│   ├── settings_local_test.py
│   ├── settings_template.py
│   ├── settings_test.py
│   ├── signals/
│   │   ├── __init__.py
│   │   ├── handlers.py
│   │   └── signals.py
│   ├── static/
│   │   ├── admin/
│   │   │   ├── custom-inline.js
│   │   │   └── editor-preview.js
│   │   ├── js/
│   │   │   └── src/
│   │   │       ├── about-accordions.js
│   │   │       ├── asset-reservation.js
│   │   │       ├── banner.js
│   │   │       ├── base.js
│   │   │       ├── campaign-selection.js
│   │   │       ├── contribute.js
│   │   │       ├── filter-assets.js
│   │   │       ├── guide.js
│   │   │       ├── homepage-carousel.js
│   │   │       ├── modules/
│   │   │       │   ├── accessible-colors.js
│   │   │       │   ├── chroma-esm.js
│   │   │       │   ├── concordia-visualization.js
│   │   │       │   ├── quick-tips.js
│   │   │       │   ├── turnstile.js
│   │   │       │   └── visualization-errors.js
│   │   │       ├── ocr.js
│   │   │       ├── password-validation.js
│   │   │       ├── profile-fields.js
│   │   │       ├── quick-tips-setup.js
│   │   │       ├── recent-pages.js
│   │   │       ├── viewer-split.js
│   │   │       ├── viewer.js
│   │   │       └── visualizations/
│   │   │           ├── asset-status-by-campaign.js
│   │   │           ├── asset-status-overview.js
│   │   │           └── daily-activity.js
│   │   ├── scss/
│   │   │   ├── _variables.scss
│   │   │   └── base.scss
│   │   └── vendor/
│   │       └── jquery.cookie.js
│   ├── storage.py
│   ├── storage_backends.py
│   ├── tasks/
│   │   ├── __init__.py
│   │   ├── assets.py
│   │   ├── blog.py
│   │   ├── housekeeping.py
│   │   ├── next_asset/
│   │   │   ├── __init__.py
│   │   │   ├── renew.py
│   │   │   ├── reviewable.py
│   │   │   └── transcribable.py
│   │   ├── reports/
│   │   │   ├── __init__.py
│   │   │   ├── backfill.py
│   │   │   ├── key_metrics.py
│   │   │   └── sitereport.py
│   │   ├── reservations.py
│   │   ├── retirement.py
│   │   ├── search_index.py
│   │   ├── thumbnails.py
│   │   ├── unusualactivity.py
│   │   ├── useractivity.py
│   │   └── visualizations.py
│   ├── templates/
│   │   ├── 404.html
│   │   ├── 429.html
│   │   ├── 500.html
│   │   ├── 503.html
│   │   ├── account/
│   │   │   ├── account_deletion.html
│   │   │   ├── email_reconfirmation_failed.html
│   │   │   └── profile.html
│   │   ├── admin/
│   │   │   ├── auth/
│   │   │   │   └── user/
│   │   │   │       └── change_form.html
│   │   │   ├── base_site.html
│   │   │   ├── bulk_change.html
│   │   │   ├── bulk_import.html
│   │   │   ├── bulk_review.html
│   │   │   ├── celery_task.html
│   │   │   ├── clear_cache.html
│   │   │   ├── concordia/
│   │   │   │   ├── asset/
│   │   │   │   │   ├── change_form.html
│   │   │   │   │   └── change_list.html
│   │   │   │   ├── campaign/
│   │   │   │   │   ├── change_form.html
│   │   │   │   │   └── retire.html
│   │   │   │   ├── item/
│   │   │   │   │   └── change_form.html
│   │   │   │   ├── project/
│   │   │   │   │   ├── change_form.html
│   │   │   │   │   └── item_import.html
│   │   │   │   ├── simplepage/
│   │   │   │   │   └── change_form.html
│   │   │   │   └── transcription/
│   │   │   │       └── change_form.html
│   │   │   ├── index.html
│   │   │   ├── long_name_filter.html
│   │   │   ├── process_bagit.html
│   │   │   └── project_level_export.html
│   │   ├── base.html
│   │   ├── django_registration/
│   │   │   ├── activation_complete.html
│   │   │   ├── activation_email_body.txt
│   │   │   ├── activation_email_subject.txt
│   │   │   ├── activation_failed.html
│   │   │   ├── registration_closed.html
│   │   │   ├── registration_complete.html
│   │   │   └── registration_form.html
│   │   ├── documents/
│   │   │   └── service_letter.html
│   │   ├── emails/
│   │   │   ├── delete_account_body.txt
│   │   │   ├── delete_account_subject.txt
│   │   │   ├── email_reconfirmation_body.txt
│   │   │   ├── email_reconfirmation_subject.txt
│   │   │   ├── unusual_activity.html
│   │   │   ├── unusual_activity.txt
│   │   │   ├── welcome_email_body.html
│   │   │   ├── welcome_email_body.txt
│   │   │   └── welcome_email_subject.txt
│   │   ├── error.html
│   │   ├── forms/
│   │   │   └── widgets/
│   │   │       ├── email.html
│   │   │       └── turnstile_widget.html
│   │   ├── fragments/
│   │   │   ├── _filter-buttons.html
│   │   │   ├── _modal_footer.html
│   │   │   ├── activity-filter-sort.html
│   │   │   ├── codemirror.html
│   │   │   ├── common-stylesheets.html
│   │   │   ├── featured_blog_posts.html
│   │   │   ├── recent-pages.html
│   │   │   ├── sharing-button-group.html
│   │   │   ├── standard-pagination.html
│   │   │   ├── transcription-progress-bar.html
│   │   │   ├── transcription-progress-row.html
│   │   │   └── transcription-status-filters.html
│   │   ├── home.html
│   │   ├── registration/
│   │   │   ├── activate.html
│   │   │   ├── login.html
│   │   │   ├── password_change_done.html
│   │   │   ├── password_change_form.html
│   │   │   ├── password_reset_complete.html
│   │   │   ├── password_reset_confirm.html
│   │   │   ├── password_reset_done.html
│   │   │   ├── password_reset_email.html
│   │   │   ├── password_reset_form.html
│   │   │   └── password_reset_subject.txt
│   │   ├── static-page.html
│   │   └── transcriptions/
│   │       ├── asset_detail/
│   │       │   ├── asset_reservation_failure_modal.html
│   │       │   ├── editor.html
│   │       │   ├── error_modal.html
│   │       │   ├── guide.html
│   │       │   ├── language_selection_modal.html
│   │       │   ├── navigation.html
│   │       │   ├── nothing_to_transcribe_modal.html
│   │       │   ├── ocr_help_modal.html
│   │       │   ├── ocr_transcription_modal.html
│   │       │   ├── quick_tips_modal.html
│   │       │   ├── review_accepted_modal.html
│   │       │   ├── successful_submission_modal.html
│   │       │   ├── tags.html
│   │       │   ├── viewer.html
│   │       │   └── viewer_filters.html
│   │       ├── asset_detail.html
│   │       ├── campaign_detail.html
│   │       ├── campaign_detail_completed.html
│   │       ├── campaign_detail_retired.html
│   │       ├── campaign_list.html
│   │       ├── campaign_list_small_blocks.html
│   │       ├── campaign_report.html
│   │       ├── campaign_small_block.html
│   │       ├── campaign_topic_list.html
│   │       ├── completed_campaigns_section.html
│   │       ├── item_detail.html
│   │       ├── project_detail.html
│   │       ├── topic_detail.html
│   │       └── transcription.html
│   ├── templatetags/
│   │   ├── __init__.py
│   │   ├── concordia_filtering_tags.py
│   │   ├── concordia_media_tags.py
│   │   ├── concordia_querystring.py
│   │   ├── concordia_sharing_tags.py
│   │   ├── concordia_text_tags.py
│   │   ├── custom_math.py
│   │   ├── group_list.py
│   │   ├── reject_filter.py
│   │   ├── truncation.py
│   │   └── visualization.py
│   ├── tests/
│   │   ├── README.md
│   │   ├── __init__.py
│   │   ├── axe.py
│   │   ├── data/
│   │   │   └── site_reports.csv
│   │   ├── test_account_views.py
│   │   ├── test_admin.py
│   │   ├── test_admin_actions.py
│   │   ├── test_admin_filters.py
│   │   ├── test_admin_forms.py
│   │   ├── test_admin_views.py
│   │   ├── test_api_views.py
│   │   ├── test_authentication.py
│   │   ├── test_celery.py
│   │   ├── test_consumers.py
│   │   ├── test_contextmanagers.py
│   │   ├── test_decorators.py
│   │   ├── test_fields.py
│   │   ├── test_logging.py
│   │   ├── test_maintenance.py
│   │   ├── test_management_commands.py
│   │   ├── test_models.py
│   │   ├── test_parser.py
│   │   ├── test_registration_views.py
│   │   ├── test_s3.py
│   │   ├── test_selenium.py
│   │   ├── test_sentry.py
│   │   ├── test_signals.py
│   │   ├── test_tasks_assets.py
│   │   ├── test_tasks_blog.py
│   │   ├── test_tasks_housekeeping.py
│   │   ├── test_tasks_next_asset.py
│   │   ├── test_tasks_reports_backfill.py
│   │   ├── test_tasks_reports_key_metrics.py
│   │   ├── test_tasks_reports_sitereport.py
│   │   ├── test_tasks_retirement.py
│   │   ├── test_tasks_search_index.py
│   │   ├── test_tasks_thumbnails.py
│   │   ├── test_tasks_unusualactivity.py
│   │   ├── test_tasks_useractivity.py
│   │   ├── test_tasks_visualizations.py
│   │   ├── test_templatetags.py
│   │   ├── test_top_level_views.py
│   │   ├── test_utils_celery.py
│   │   ├── test_utils_logging.py
│   │   ├── test_utils_next_asset_reviewable_campaign.py
│   │   ├── test_utils_next_asset_reviewable_topic.py
│   │   ├── test_utils_next_asset_transcribable_campaign.py
│   │   ├── test_utils_next_asset_transcribable_topic.py
│   │   ├── test_validators.py
│   │   ├── test_view_decorators.py
│   │   ├── test_views.py
│   │   ├── test_views_asset_reservation.py
│   │   ├── test_views_redirect_next_reviewable.py
│   │   ├── test_views_redirect_next_transcribable.py
│   │   ├── test_views_tags.py
│   │   ├── test_views_topics.py
│   │   ├── test_views_transcription_review.py
│   │   ├── test_views_transcription_save.py
│   │   ├── test_views_transcription_submit.py
│   │   ├── test_views_utils.py
│   │   ├── test_widgets.py
│   │   └── utils.py
│   ├── turnstile/
│   │   ├── LICENSE
│   │   ├── __init__.py
│   │   ├── context_processors.py
│   │   ├── fields.py
│   │   └── widgets.py
│   ├── urls.py
│   ├── utils/
│   │   ├── __init__.py
│   │   ├── celery.py
│   │   ├── constants.py
│   │   └── next_asset/
│   │       ├── __init__.py
│   │       ├── reviewable/
│   │       │   ├── __init__.py
│   │       │   ├── campaign.py
│   │       │   └── topic.py
│   │       └── transcribable/
│   │           ├── __init__.py
│   │           ├── campaign.py
│   │           └── topic.py
│   ├── validators.py
│   ├── version.py
│   ├── views/
│   │   ├── README.md
│   │   ├── __init__.py
│   │   ├── accounts.py
│   │   ├── ajax.py
│   │   ├── assets.py
│   │   ├── campaigns.py
│   │   ├── decorators.py
│   │   ├── items.py
│   │   ├── maintenance_mode.py
│   │   ├── projects.py
│   │   ├── rate_limit.py
│   │   ├── simple_pages.py
│   │   ├── topics.py
│   │   ├── utils.py
│   │   └── visualizations.py
│   ├── widgets.py
│   └── wsgi.py
├── configuration/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── management/
│   │   ├── __init__.py
│   │   └── commands/
│   │       ├── __init__.py
│   │       └── configcache.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   ├── 0002_populate_configurations.py
│   │   ├── 0003_populate_retry_configurations.py
│   │   ├── 0004_alter_configuration_options.py
│   │   ├── 0005_alter_configuration_data_type.py
│   │   ├── 0006_populate_next_asset_rate_limit.py
│   │   └── __init__.py
│   ├── models.py
│   ├── signals.py
│   ├── templates/
│   │   └── admin/
│   │       └── configuration_confirm_update.html
│   ├── templatetags/
│   │   ├── __init__.py
│   │   └── configuration_tags.py
│   ├── tests/
│   │   ├── __init__.py
│   │   ├── test_admin.py
│   │   ├── test_models.py
│   │   ├── test_signals.py
│   │   ├── test_templatetags.py
│   │   ├── test_utils.py
│   │   └── test_validation.py
│   ├── utils.py
│   ├── validation.py
│   └── views.py
├── db_scripts/
│   ├── Dockerfile
│   ├── dump.sh
│   └── restore.sh
├── development/
│   ├── Containerfile
│   ├── README.md
│   └── compose.yml
├── docker-compose.yml
├── docs/
│   ├── accessibility-goals.md
│   ├── accessibility-techniques.md
│   ├── design-principles.md
│   ├── for-developers.md
│   └── how-we-work.md
├── entrypoint.sh
├── exporter/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── exceptions.py
│   ├── migrations/
│   │   └── __init__.py
│   ├── models.py
│   ├── tabular_export/
│   │   ├── admin.py
│   │   └── core.py
│   ├── templates/
│   │   └── admin/
│   │       └── exporter/
│   │           └── unacceptable_character_report.html
│   ├── tests/
│   │   ├── __init__.py
│   │   ├── test_exceptions.py
│   │   ├── test_tabular_export.py
│   │   ├── test_utils.py
│   │   └── test_views.py
│   ├── utils.py
│   └── views.py
├── fixtures/
│   └── original-static-pages.json
├── frontend/
│   ├── .gitignore
│   ├── README.md
│   ├── eslint.config.js
│   ├── index.html
│   ├── package.json
│   ├── src/
│   │   ├── App.jsx
│   │   ├── ViewerSplit.jsx
│   │   ├── config.js
│   │   ├── editor/
│   │   │   ├── Buttons.jsx
│   │   │   ├── Editor.jsx
│   │   │   ├── Header.jsx
│   │   │   ├── StatusMessages.jsx
│   │   │   ├── TranscriptionTextarea.jsx
│   │   │   └── buttons/
│   │   │       ├── Editable.jsx
│   │   │       ├── Redo.jsx
│   │   │       ├── Review.jsx
│   │   │       ├── Save.jsx
│   │   │       ├── Submit.jsx
│   │   │       └── Undo.jsx
│   │   ├── main.jsx
│   │   ├── ocr/
│   │   │   ├── Button.jsx
│   │   │   ├── ConfirmModal.jsx
│   │   │   ├── Handler.jsx
│   │   │   ├── HelpModal.jsx
│   │   │   ├── LanguageModal.jsx
│   │   │   └── Section.jsx
│   │   └── viewer/
│   │       ├── Controls.jsx
│   │       ├── FilterTabNav.jsx
│   │       ├── GammaFilterForm.jsx
│   │       ├── ImageFilters.jsx
│   │       ├── InvertFilterForm.jsx
│   │       ├── KeyboardHelpModal.jsx
│   │       ├── KeyboardShortcutRow.jsx
│   │       ├── ThresholdFilterForm.jsx
│   │       └── Viewer.jsx
│   └── vite.config.js
├── importer/
│   ├── Dockerfile
│   ├── README.md
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── celery.py
│   ├── config.py
│   ├── entrypoint.sh
│   ├── exceptions.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   ├── 0001_squashed_0015_auto_20180925_1851.py
│   │   ├── 0002_auto_20180709_0833.py
│   │   ├── 0003_auto_20180709_0933.py
│   │   ├── 0004_auto_20180812_1007.py
│   │   ├── 0005_auto_20180816_1702.py
│   │   ├── 0006_auto_20180912_0229.py
│   │   ├── 0007_auto_20180917_1654.py
│   │   ├── 0008_campaigntaskdetails_project.py
│   │   ├── 0009_convert_project_text_to_keys.py
│   │   ├── 0010_auto_20180920_2013.py
│   │   ├── 0011_auto_20180922_0208.py
│   │   ├── 0012_auto_20180923_0231.py
│   │   ├── 0013_auto_20180924_1318.py
│   │   ├── 0014_auto_20180924_1943.py
│   │   ├── 0015_auto_20180925_1851.py
│   │   ├── 0016_importitem_failure_reason_and_more.py
│   │   ├── 0017_importitem_failure_history_importitem_retry_count_and_more.py
│   │   ├── 0018_importitem_status_history_and_more.py
│   │   ├── 0019_alter_downloadassetimagejob_batch_and_more.py
│   │   ├── 0020_alter_downloadassetimagejob_unique_together_and_more.py
│   │   └── __init__.py
│   ├── models.py
│   ├── setup.py
│   ├── tasks/
│   │   ├── __init__.py
│   │   ├── assets.py
│   │   ├── collections.py
│   │   ├── decorators.py
│   │   ├── images.py
│   │   └── items.py
│   ├── tests/
│   │   ├── README.md
│   │   ├── __init__.py
│   │   ├── test_admin.py
│   │   ├── test_celery.py
│   │   ├── test_models.py
│   │   ├── test_tasks_assets.py
│   │   ├── test_tasks_collections.py
│   │   ├── test_tasks_core.py
│   │   ├── test_tasks_decorators.py
│   │   ├── test_tasks_images.py
│   │   ├── test_tasks_items.py
│   │   ├── test_utils.py
│   │   └── utils.py
│   └── utils/
│       ├── __init__.py
│       ├── excel.py
│       └── verify_images.py
├── load_test.sh
├── locustfile.py
├── manage.py
├── package.json
├── postgresql/
│   └── create-multiple-postgresql-databases.sh
├── prometheus_metrics/
│   ├── LICENSE
│   ├── __init__.py
│   ├── apps.py
│   ├── middleware.py
│   ├── models.py
│   └── views.py
├── pylenium.json
├── pyproject.toml
├── setup.cfg
├── setup.py
├── src/
│   ├── about.js
│   ├── main.js
│   └── profile.js
├── static/
│   └── .gitignore
├── tools/
│   └── readme_symbol_check.py
└── vite.config.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .cfnlintrc.yaml
================================================
# The W2001 check is used to ignore the featurebranch.yaml DataLoadStackName parameter in the nested
# stack fargate-featurebranch.yaml used to signal when the DataLoadHost UserData commands are complete.
# Check if Parameters are Used
ignore_checks:
    - W2001


================================================
FILE: .dockerignore
================================================
node_modules
static-files


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
---

**What behavior did you observe? Please describe the bug**
A clear and concise description of what you experienced.

**How can we reproduce the bug?**
Steps to reproduce the behavior:

1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**What is the expected behavior?**
A clear and concise description of what you expected to happen.

**Got screenshots? This helps us identify the issue**
Add screenshots to help explain your problem.

**Desktop (please complete the following information):**

-   OS: [e.g. iOS]
-   Browser [e.g. chrome, safari]

**Additional context**
Add any other context about the problem here.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
---

**User story/persona**
As {a user}, I want to {action} so that I can {goal}

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Additional context**
Add any other context or screenshots about the feature request here.

**Acceptance Criteria**
Add a list items this new feature needs to meet. Ex: The user would not be able to submit a form if all the mandatory fields are not entered.

**Acceptance Test:**
Add a list of steps to test for a user to check if functionality satisfies the acceptance criteria.


================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

version: 2
updates:
    - package-ecosystem: 'github-actions' # See documentation for possible values
      directory: '/workflows' # Location of package manifests
      schedule:
          interval: 'weekly'
    - package-ecosystem: 'npm' # See documentation for possible values
      directory: '/' # Location of package manifests
      schedule:
          interval: 'daily'
    - package-ecosystem: 'pip' # See documentation for possible values
      directory: '/' # Location of package manifests
      schedule:
          interval: 'daily'


================================================
FILE: .github/workflows/black.yml
================================================
name: Lint

on:
    workflow_dispatch:
    pull_request:
        branches: [main, 'feature-*', release]
        paths-ignore:
            - docs/**
            - README.md
            - .github/**
            - cloudformation/**
            - db_scripts/**
            - jenkins/**
            - search-proxy/**
            - postgresql/**

jobs:
    lint:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v6
            - uses: actions/setup-python@v6
              with:
                  python-version: '3.12'
            - uses: psf/black@stable


================================================
FILE: .github/workflows/build.yml
================================================
name: 'Build'

on:
    workflow_dispatch:
    pull_request:
        branches: [main, 'feature-*', release]
        paths-ignore:
            - docs/**
            - README.md
            - .github/**
            - cloudformation/**
            - db_scripts/**
            - jenkins/**
            - search-proxy/**
            - postgresql/**

jobs:
    build:
        name: Build
        runs-on: ubuntu-latest

        steps:
            - name: Install system packages
              run: |
                  sudo apt-get update -qy && sudo apt-get dist-upgrade -qy && sudo apt-get install -qy \
                  libmemcached-dev libz-dev libfreetype6-dev libtiff-dev \
                  libjpeg-dev libopenjp2-7-dev libwebp-dev zlib1g-dev libpq-dev

            - name: Install node and npm
              uses: actions/setup-node@v6
              with:
                  node-version: '20'

            - name: Checkout repository
              uses: actions/checkout@v6

            - name: Set up Python 3.12
              uses: actions/setup-python@v6
              with:
                  # Semantic version range syntax or exact version of a Python version
                  python-version: '3.12'
                  # Optional - x64 or x86 architecture, defaults to x64
                  architecture: 'x64'

            - name: Display Python version
              run: python -c "import sys; print(sys.version)"

            - name: build containers
              run: |
                  docker build -t concordia .
                  docker build -t concordia/importer --file importer/Dockerfile .
                  docker build -t concordia/celerybeat --file celerybeat/Dockerfile .


================================================
FILE: .github/workflows/codeql.yml
================================================
name: 'CodeQL Advanced'

on:
    workflow_dispatch:
    push:
        branches: [main, 'feature-*']
    pull_request:
        branches: [main, 'feature-*', release]
        paths-ignore:
            - docs/**
            - README.md
            - cloudformation/**
            - db_scripts/**
            - jenkins/**
            - search-proxy/**
            - postgresql/**
    schedule:
        - cron: '20 23 * * 2'

jobs:
    analyze:
        name: Analyze (${{ matrix.language }})
        runs-on: ubuntu-latest

        permissions:
            actions: read
            contents: read
            security-events: write
            packages: read

        strategy:
            fail-fast: false
            matrix:
                include:
                    - language: javascript-typescript
                      build-mode: none
                    - language: python
                      build-mode: none

        steps:
            - name: Install system packages
              run: |
                  sudo apt-get update -qy && sudo apt-get dist-upgrade -qy && sudo apt-get install -qy \
                  libmemcached-dev libz-dev libfreetype6-dev libtiff-dev \
                  libjpeg-dev libopenjp2-7-dev libwebp-dev zlib1g-dev libpq-dev

            - name: Checkout repository
              uses: actions/checkout@v6

            - if: matrix.language == 'python'
              name: Setup python
              uses: actions/setup-python@v6
              with:
                  python-version: '3.12'

            # Initializes the CodeQL tools for scanning.
            - name: Initialize CodeQL
              uses: github/codeql-action/init@v4
              with:
                  languages: ${{ matrix.language }}
                  build-mode: ${{ matrix.build-mode }}

            - if: matrix.language == 'python'
              run: |
                  pip install -U packaging
                  pip install -U setuptools
                  pip install pipenv
                  pipenv install --dev --deploy

            - name: Perform CodeQL Analysis
              uses: github/codeql-action/analyze@v4
              with:
                  category: '/language:${{matrix.language}}'


================================================
FILE: .github/workflows/db_ops.yml
================================================
name: DB Operations Multi-Repo Pipeline

on:
    workflow_dispatch:
        inputs:
            action_type:
                description: 'Action'
                required: true
                default: 'build_test'
                type: choice
                options:
                    - build_test
                    - promote_to_latest
            operation:
                description: 'Operation'
                required: true
                default: 'dump'
                type: choice
                options:
                    - dump
                    - restore

env:
    AWS_REGION: us-east-1
    # Mapping the operation to the specific ECR Repo Name
    DUMP_REPO: crowd-db-dump
    RESTORE_REPO: crowd-db-restore

jobs:
    process:
        runs-on: ubuntu-latest
        steps:
            - name: Checkout Code
              uses: actions/checkout@v6

            - name: Configure AWS Credentials
              uses: aws-actions/configure-aws-credentials@v6
              with:
                  aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
                  aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
                  aws-region: ${{ env.AWS_REGION }}
                  role-session-name: github_to_aws_deploy

            - name: Login to Amazon ECR
              id: login-ecr
              uses: aws-actions/amazon-ecr-login@v2

            # LOGIC: Determine Repo Name and Docker Stage Target
            - name: Set Variables
              id: vars
              run: |
                  if [[ "${{ github.event.inputs.operation }}" == "dump" ]]; then
                    echo "REPO_NAME=${{ env.DUMP_REPO }}" >> $GITHUB_OUTPUT
                    echo "STAGE_TARGET=dump" >> $GITHUB_OUTPUT
                  else
                    echo "REPO_NAME=${{ env.RESTORE_REPO }}" >> $GITHUB_OUTPUT
                    echo "STAGE_TARGET=restore" >> $GITHUB_OUTPUT
                  fi

            # ACTION 1: BUILD AND PUSH 'test'
            - name: Build and Push Test
              if: ${{ github.event.inputs.action_type == 'build_test' }}
              uses: docker/build-push-action@v7
              with:
                  # context: defines where the 'COPY' commands look for files
                  context: ./db_scripts
                  # file: path to the actual Dockerfile relative to repo root
                  file: ./db_scripts/Dockerfile
                  # target: tells Docker to stop at the 'dump' or 'restore' stage
                  target: ${{ steps.vars.outputs.STAGE_TARGET }}
                  push: true
                  tags: ${{ steps.login-ecr.outputs.registry }}/${{ steps.vars.outputs.REPO_NAME }}:test

            # ACTION 2: PROMOTE 'test' to 'latest'
            - name: Promote Test to Latest
              if: ${{ github.event.inputs.action_type == 'promote_to_latest' }}
              run: |
                  REPO=${{ steps.vars.outputs.REPO_NAME }}

                  MANIFEST=$(aws ecr batch-get-image \
                    --repository-name $REPO \
                    --image-ids imageTag=test \
                    --query 'images[0].imageManifest' \
                    --output text)

                  aws ecr put-image \
                    --repository-name $REPO \
                    --image-tag latest \
                    --image-manifest "$MANIFEST"


================================================
FILE: .github/workflows/dev-main-deploy.yml
================================================
name: 'Deploy to dev'

on:
    workflow_dispatch:
    push:
        branches: [main]
        paths-ignore:
            - docs/**
            - README.md
            - .github/**
            - cloudformation/**
            - db_scripts/**
            - jenkins/**
            - search-proxy/**
            - postgresql/**
            - cloudformation/tests/**
            - concordia/tests/**
            - exporter/tests/**
            - importer/tests/**

env:
    AWS_REGION: us-east-1

permissions:
    id-token: write
    contents: read

jobs:
    deploy:
        name: Deploy to Dev
        runs-on: ubuntu-latest
        environment:
            name: development

        steps:
            - name: Install system packages
              run: |
                  sudo apt-get update -qy && sudo apt-get dist-upgrade -qy && sudo apt-get install -qy \
                  libmemcached-dev libz-dev libfreetype6-dev libtiff-dev \
                  libjpeg-dev libopenjp2-7-dev libwebp-dev zlib1g-dev libpq-dev

            - name: Install node and npm
              uses: actions/setup-node@v6
              with:
                  node-version: '20'

            - name: Checkout repository
              uses: actions/checkout@v6
              with:
                  fetch-depth: 0
                  fetch-tags: 'true'

            - name: Set up Python 3.12
              uses: actions/setup-python@v6
              with:
                  # Semantic version range syntax or exact version of a Python version
                  python-version: '3.12'
                  # Optional - x64 or x86 architecture, defaults to x64
                  architecture: 'x64'

            - name: Install Python Dependencies and Retrieve Version Number
              id: python-build
              run: |
                  python3 -m pip install --upgrade pip
                  pip3 install -U setuptools
                  pip3 install -U setuptools-scm

                  FULL_VERSION_NUMBER="$(python3 -m setuptools_scm)"
                  echo "version_number=$(echo "${FULL_VERSION_NUMBER}" | cut -d '+' -f 1)" >> $GITHUB_ENV

            - name: configure aws credentials
              uses: aws-actions/configure-aws-credentials@v6
              with:
                  aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
                  aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
                  aws-region: ${{ env.AWS_REGION }}
                  role-session-name: github_to_aws_deploy

            - name: Login to Amazon ECR
              id: login-ecr
              uses: aws-actions/amazon-ecr-login@v2

            - name: Build, tag and push docker images ECR
              env:
                  REGISTRY: ${{ steps.login-ecr.outputs.registry }}
                  IMAGE_TAG: ${{ secrets.IMAGE_TAG }}
                  CLUSTER: ${{ secrets.CLUSTER }}
                  TARGET_SERVICE: ${{ secrets.TARGET_SERVICE }}
              run: |
                  docker build -t concordia .
                  docker build -t concordia/importer --file importer/Dockerfile .
                  docker build -t concordia/celerybeat --file celerybeat/Dockerfile .

                  docker tag concordia:latest $REGISTRY/concordia:$version_number
                  docker tag concordia:latest $REGISTRY/concordia:$IMAGE_TAG
                  docker tag concordia/importer:latest $REGISTRY/concordia/importer:$version_number
                  docker tag concordia/importer:latest $REGISTRY/concordia/importer:$IMAGE_TAG
                  docker tag concordia/celerybeat:latest $REGISTRY/concordia/celerybeat:$version_number
                  docker tag concordia/celerybeat:latest $REGISTRY/concordia/celerybeat:$IMAGE_TAG

                  docker push $REGISTRY/concordia:$version_number
                  docker push $REGISTRY/concordia:$IMAGE_TAG
                  docker push $REGISTRY/concordia/importer:$version_number
                  docker push $REGISTRY/concordia/importer:$IMAGE_TAG
                  docker push $REGISTRY/concordia/celerybeat:$version_number
                  docker push $REGISTRY/concordia/celerybeat:$IMAGE_TAG

                  aws ecs update-service --region ${{ env.AWS_REGION }} --force-new-deployment --cluster $CLUSTER --service $TARGET_SERVICE


================================================
FILE: .github/workflows/feature-branch-deploy.yml
================================================
name: 'Deploy feature branch to test'

on:
    workflow_dispatch:
    push:
        branches: ['feature-*']
        paths-ignore:
            - docs/**
            - README.md
            - .github/**
            - cloudformation/**
            - db_scripts/**
            - jenkins/**
            - search-proxy/**
            - postgresql/**

env:
    AWS_REGION: us-east-1

permissions:
    id-token: write
    contents: read

jobs:
    deploy:
        name: Deploy Feature Branch to Test
        runs-on: ubuntu-latest
        environment:
            name: feature
        steps:
            - name: Install system packages
              run: |
                  sudo apt-get update -qy && sudo apt-get dist-upgrade -qy && sudo apt-get install -qy \
                  libmemcached-dev libz-dev libfreetype6-dev libtiff-dev \
                  libjpeg-dev libopenjp2-7-dev libwebp-dev zlib1g-dev libpq-dev

            - name: Install node and npm
              uses: actions/setup-node@v6
              with:
                  node-version: '20'

            - name: Checkout repository
              uses: actions/checkout@v6
              with:
                  ref: ${{ vars.FEATURE_BRANCH }}
                  fetch-depth: 0
                  fetch-tags: 'true'

            - name: Set up Python 3.12
              uses: actions/setup-python@v6
              with:
                  # Semantic version range syntax or exact version of a Python version
                  python-version: '3.12'
                  # Optional - x64 or x86 architecture, defaults to x64
                  architecture: 'x64'

            - name: Install Python Dependencies and Retrieve Version Number
              id: python-build
              run: |
                  python3 -m pip install --upgrade pip
                  pip3 install -U setuptools
                  pip3 install -U setuptools-scm

                  FULL_VERSION_NUMBER="$(python3 -m setuptools_scm)"
                  echo "version_number=$(echo "${FULL_VERSION_NUMBER}" | cut -d '+' -f 1)" >> $GITHUB_ENV

            - name: configure aws credentials
              uses: aws-actions/configure-aws-credentials@v6
              with:
                  aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
                  aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
                  aws-region: ${{ env.AWS_REGION }}
                  role-session-name: github_to_aws_deploy

            - name: Login to Amazon ECR
              id: login-ecr
              uses: aws-actions/amazon-ecr-login@v2

            - name: Build, tag and push docker images ECR
              env:
                  REGISTRY: ${{ steps.login-ecr.outputs.registry }}
                  IMAGE_TAG: ${{ secrets.IMAGE_TAG }}
                  CLUSTER: ${{ secrets.CLUSTER }}
                  TARGET_SERVICE: ${{ secrets.TARGET_SERVICE }}
              run: |
                  docker build -t concordia .
                  docker build -t concordia/importer --file importer/Dockerfile .
                  docker build -t concordia/celerybeat --file celerybeat/Dockerfile .

                  docker tag concordia:latest $REGISTRY/concordia:$version_number
                  docker tag concordia:latest $REGISTRY/concordia:$IMAGE_TAG
                  docker tag concordia/importer:latest $REGISTRY/concordia/importer:$version_number
                  docker tag concordia/importer:latest $REGISTRY/concordia/importer:$IMAGE_TAG
                  docker tag concordia/celerybeat:latest $REGISTRY/concordia/celerybeat:$version_number
                  docker tag concordia/celerybeat:latest $REGISTRY/concordia/celerybeat:$IMAGE_TAG

                  docker push $REGISTRY/concordia:$version_number
                  docker push $REGISTRY/concordia:$IMAGE_TAG
                  docker push $REGISTRY/concordia/importer:$version_number
                  docker push $REGISTRY/concordia/importer:$IMAGE_TAG
                  docker push $REGISTRY/concordia/celerybeat:$version_number
                  docker push $REGISTRY/concordia/celerybeat:$IMAGE_TAG

                  aws ecs update-service --region ${{ env.AWS_REGION }} --force-new-deployment --cluster $CLUSTER --service $TARGET_SERVICE


================================================
FILE: .github/workflows/pip-audit.yml
================================================
name: pip-audit

on:
    workflow_dispatch:
    pull_request:
        branches: [main, release]
        paths-ignore:
            - docs/**
            - README.md
            - .github/**
            - cloudformation/**
            - db_scripts/**
            - jenkins/**
            - search-proxy/**
            - postgresql/**

jobs:
    pip-audit:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v6
            - uses: actions/setup-python@v6
              with:
                  python-version: '3.12'

            - name: 'Generate requirements.txt'
              run: |
                  pipx run pipfile-requirements Pipfile.lock > requirements.txt

            - uses: pypa/gh-action-pip-audit@v1.1.0
              with:
                  inputs: requirements.txt
                  ignore-vulns: |
                      PYSEC-2023-312


================================================
FILE: .github/workflows/prod-deploy.yml
================================================
name: 'Deploy to production'

on:
    workflow_dispatch:

env:
    AWS_REGION: us-east-1

permissions:
    id-token: write
    contents: read

jobs:
    deploy:
        name: Deploy to Production
        runs-on: ubuntu-latest
        environment:
            name: production

        steps:
            - name: configure aws credentials
              uses: aws-actions/configure-aws-credentials@v6
              with:
                  aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
                  aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
                  aws-region: ${{ env.AWS_REGION }}
                  role-session-name: github_to_aws_deploy

            - name: Login to Amazon ECR
              id: login-ecr
              uses: aws-actions/amazon-ecr-login@v2

            - name: Pull, tag and push docker images ECR
              env:
                  REGISTRY: ${{ steps.login-ecr.outputs.registry }}
                  IMAGE_TAG_PULL: ${{ secrets.IMAGE_TAG_PULL }}
                  IMAGE_TAG: ${{ secrets.IMAGE_TAG }}
                  CLUSTER: ${{ secrets.CLUSTER }}
                  TARGET_SERVICE_A: ${{ secrets.TARGET_SERVICE_A }}
                  TARGET_SERVICE: ${{ secrets.TARGET_SERVICE }}
              run: |
                  docker pull $REGISTRY/concordia:$IMAGE_TAG_PULL
                  docker pull $REGISTRY/concordia/importer:$IMAGE_TAG_PULL
                  docker pull $REGISTRY/concordia/celerybeat:$IMAGE_TAG_PULL

                  docker tag $REGISTRY/concordia:$IMAGE_TAG_PULL $REGISTRY/concordia:$IMAGE_TAG
                  docker tag $REGISTRY/concordia/importer:$IMAGE_TAG_PULL $REGISTRY/concordia/importer:$IMAGE_TAG
                  docker tag $REGISTRY/concordia/celerybeat:$IMAGE_TAG_PULL $REGISTRY/concordia/celerybeat:$IMAGE_TAG

                  docker push $REGISTRY/concordia:$IMAGE_TAG
                  docker push $REGISTRY/concordia/importer:$IMAGE_TAG
                  docker push $REGISTRY/concordia/celerybeat:$IMAGE_TAG

                  aws ecs update-service --region ${{ env.AWS_REGION }} --force-new-deployment --cluster $CLUSTER --service $TARGET_SERVICE_A
                  aws ecs update-service --region ${{ env.AWS_REGION }} --force-new-deployment --cluster $CLUSTER --service $TARGET_SERVICE


================================================
FILE: .github/workflows/renew_coverage.yml
================================================
name: Renew Coverage Cache

on:
    schedule:
        - cron: '0 0 */5 * *' # Runs every 5 days at midnight UTC
    workflow_dispatch:

        # The renew_coverage.yml action is used to keep the cached release coverage value by
        #  accessing it every five days. Normally, cached values are discarded after they're
        #  not accessed for seven days. To avoid that, the task simply accessing the value so
        #  it's not lost in case we have a seven-day period with no pull requests.

jobs:
    renew-cache:
        runs-on: ubuntu-latest
        steps:
            - name: Access Coverage Cache to Renew Expiration
              uses: actions/cache@v5
              with:
                  path: coverage.txt
                  key: release-coverage
                  restore-keys: |
                      release-coverage


================================================
FILE: .github/workflows/stage-hotfix-rel-deploy.yml
================================================
name: 'Deploy hotfix to stage'

on:
    workflow_dispatch:

env:
    AWS_REGION: us-east-1

permissions:
    id-token: write
    contents: read

jobs:
    deploy:
        name: Deploy Release to Stage
        runs-on: ubuntu-latest
        environment:
            name: stage

        steps:
            - name: Install system packages
              run: |
                  sudo apt-get update -qy && sudo apt-get dist-upgrade -qy && sudo apt-get install -qy \
                  libmemcached-dev libz-dev libfreetype6-dev libtiff-dev \
                  libjpeg-dev libopenjp2-7-dev libwebp-dev zlib1g-dev libpq-dev

            - name: Install node and npm
              uses: actions/setup-node@v6
              with:
                  node-version: '20'

            - name: Checkout repository
              uses: actions/checkout@v6
              with:
                  ref: release
                  fetch-depth: 0
                  fetch-tags: 'true'

            - name: Set up Python 3.12
              uses: actions/setup-python@v6
              with:
                  # Semantic version range syntax or exact version of a Python version
                  python-version: '3.12'
                  # Optional - x64 or x86 architecture, defaults to x64
                  architecture: 'x64'

            - name: Get version from Git
              run: |
                  # Get latest version tag number (e.g. release was tagged in GitHub for this hot fix)
                  HOTFIX_VERSION_NUMBER="$(git describe --tags)"
                  echo "version_number=$(echo "${HOTFIX_VERSION_NUMBER}" | cut -d '-' -f 1 | cut -c 2- )" >> $GITHUB_ENV

            - name: configure aws credentials
              uses: aws-actions/configure-aws-credentials@v6
              with:
                  aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
                  aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
                  aws-region: ${{ env.AWS_REGION }}
                  role-session-name: github_to_aws_deploy

            - name: Login to Amazon ECR
              id: login-ecr
              uses: aws-actions/amazon-ecr-login@v2

            - name: Build, tag and push docker images ECR
              env:
                  REGISTRY: ${{ steps.login-ecr.outputs.registry }}
                  IMAGE_TAG: ${{ secrets.IMAGE_TAG }}
                  CLUSTER: ${{ secrets.CLUSTER }}
                  TARGET_SERVICE: ${{ secrets.TARGET_SERVICE }}
              run: |
                  echo "version number: $version_number"

                  docker build -t concordia .
                  docker build -t concordia/importer --file importer/Dockerfile .
                  docker build -t concordia/celerybeat --file celerybeat/Dockerfile .

                  docker tag concordia:latest $REGISTRY/concordia:$version_number
                  docker tag concordia:latest $REGISTRY/concordia:$IMAGE_TAG
                  docker tag concordia/importer:latest $REGISTRY/concordia/importer:$version_number
                  docker tag concordia/importer:latest $REGISTRY/concordia/importer:$IMAGE_TAG
                  docker tag concordia/celerybeat:latest $REGISTRY/concordia/celerybeat:$version_number
                  docker tag concordia/celerybeat:latest $REGISTRY/concordia/celerybeat:$IMAGE_TAG

                  docker push $REGISTRY/concordia:$version_number
                  docker push $REGISTRY/concordia:$IMAGE_TAG
                  docker push $REGISTRY/concordia/importer:$version_number
                  docker push $REGISTRY/concordia/importer:$IMAGE_TAG
                  docker push $REGISTRY/concordia/celerybeat:$version_number
                  docker push $REGISTRY/concordia/celerybeat:$IMAGE_TAG

                  aws ecs update-service --region ${{ env.AWS_REGION }} --force-new-deployment --cluster $CLUSTER --service $TARGET_SERVICE


================================================
FILE: .github/workflows/stage-image-refresh.yml
================================================
name: 'Deploy image refresh to stage'

on:
    workflow_dispatch:

env:
    AWS_REGION: us-east-1

permissions:
    id-token: write
    contents: read

jobs:
    deploy:
        name: Deploy Container Environment Update
        runs-on: ubuntu-latest
        environment:
            name: stage

        steps:
            - name: Install system packages
              run: |
                  sudo apt-get update -qy && sudo apt-get dist-upgrade -qy && sudo apt-get install -qy \
                  libmemcached-dev libz-dev libfreetype6-dev libtiff-dev \
                  libjpeg-dev libopenjp2-7-dev libwebp-dev zlib1g-dev libpq-dev

            - name: Install node and npm
              uses: actions/setup-node@v6
              with:
                  node-version: '20'

            - name: Checkout repository
              uses: actions/checkout@v6
              with:
                  ref: release
                  fetch-depth: 0
                  fetch-tags: 'true'

            - name: Set up Python 3.12
              uses: actions/setup-python@v6
              with:
                  # Semantic version range syntax or exact version of a Python version
                  python-version: '3.12'
                  # Optional - x64 or x86 architecture, defaults to x64
                  architecture: 'x64'

            - name: Create image tags
              run: |
                  # Get latest version tag number (e.g. main was tagged in GitHub for this Release)
                  FULL_VERSION_NUMBER="$(git describe --tags `git rev-list --tags --max-count=1`)"
                  echo "version_number=$(echo "${FULL_VERSION_NUMBER}" | cut -c2- )" >> $GITHUB_ENV

                  # Create image tag for image being being replaced/refreshed/updated
                  echo "tag_stale_image=$(echo "${FULL_VERSION_NUMBER}" | cut -c2- )_$(date +%Y%m%dT%H%M%S)" >> $GITHUB_ENV

            - name: configure aws credentials
              uses: aws-actions/configure-aws-credentials@v6
              with:
                  aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
                  aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
                  aws-region: ${{ env.AWS_REGION }}
                  role-session-name: github_to_aws_deploy

            - name: Login to Amazon ECR
              id: login-ecr
              uses: aws-actions/amazon-ecr-login@v2

            - name: Build, tag and push docker images ECR
              env:
                  REGISTRY: ${{ steps.login-ecr.outputs.registry }}
                  IMAGE_TAG: ${{ secrets.IMAGE_TAG }}
                  CLUSTER: ${{ secrets.CLUSTER }}
                  TARGET_SERVICE: ${{ secrets.TARGET_SERVICE }}
              run: |
                  docker build -t concordia .
                  docker build -t concordia/importer --file importer/Dockerfile .
                  docker build -t concordia/celerybeat --file celerybeat/Dockerfile .

                  docker tag concordia:latest $REGISTRY/concordia:$version_number
                  docker tag concordia:latest $REGISTRY/concordia:$IMAGE_TAG
                  docker tag concordia/importer:latest $REGISTRY/concordia/importer:$version_number
                  docker tag concordia/importer:latest $REGISTRY/concordia/importer:$IMAGE_TAG
                  docker tag concordia/celerybeat:latest $REGISTRY/concordia/celerybeat:$version_number
                  docker tag concordia/celerybeat:latest $REGISTRY/concordia/celerybeat:$IMAGE_TAG

                  docker push $REGISTRY/concordia:$version_number
                  docker push $REGISTRY/concordia:$IMAGE_TAG
                  docker push $REGISTRY/concordia/importer:$version_number
                  docker push $REGISTRY/concordia/importer:$IMAGE_TAG
                  docker push $REGISTRY/concordia/celerybeat:$version_number
                  docker push $REGISTRY/concordia/celerybeat:$IMAGE_TAG

                  aws ecs update-service --region ${{ env.AWS_REGION }} --force-new-deployment --cluster $CLUSTER --service $TARGET_SERVICE

            - name: Tag existing images
              env:
                  IT_TAG: ${{ secrets.IT_IMAGE_TAG }}
              run: |

                  # Add a new tag to existing concordia images to preserve the history of images after final deployment
                  # Tag concordia
                  APP_MANIFEST="$(aws ecr batch-get-image --repository-name concordia --image-ids imageTag=${IT_TAG} --output json | jq --raw-output --join-output '.images[0].imageManifest')"
                  aws ecr put-image --repository-name concordia --image-tag $tag_stale_image --image-manifest "$APP_MANIFEST"

                  # Tag concordia/celerybeat
                  BEAT_MANIFEST="$(aws ecr batch-get-image --repository-name concordia/celerybeat --image-ids imageTag=${IT_TAG} --output json | jq --raw-output --join-output '.images[0].imageManifest')"
                  aws ecr put-image --repository-name concordia/celerybeat --image-tag $tag_stale_image --image-manifest "$BEAT_MANIFEST"

                  # Tag concordia/importer
                  IMPORT_MANIFEST="$(aws ecr batch-get-image --repository-name concordia/importer --image-ids imageTag=${IT_TAG} --output json | jq --raw-output --join-output '.images[0].imageManifest')"
                  aws ecr put-image --repository-name concordia/importer --image-tag $tag_stale_image --image-manifest "$IMPORT_MANIFEST"


================================================
FILE: .github/workflows/stage-release-deploy.yml
================================================
name: 'Deploy release to stage'

on:
    workflow_dispatch:
    push:
        branches: [release]
        paths-ignore:
            - docs/**
            - README.md
            - .github/**
            - cloudformation/**
            - db_scripts/**
            - jenkins/**
            - search-proxy/**
            - postgresql/**
            - cloudformation/tests/**
            - concordia/tests/**
            - exporter/tests/**
            - importer/tests/**

env:
    AWS_REGION: us-east-1

permissions:
    id-token: write
    contents: read

jobs:
    deploy:
        name: Deploy Release to Stage
        runs-on: ubuntu-latest
        environment:
            name: stage

        steps:
            - name: Install system packages
              run: |
                  sudo apt-get update -qy && sudo apt-get dist-upgrade -qy && sudo apt-get install -qy \
                  libmemcached-dev libz-dev libfreetype6-dev libtiff-dev \
                  libjpeg-dev libopenjp2-7-dev libwebp-dev zlib1g-dev libpq-dev

            - name: Install node and npm
              uses: actions/setup-node@v6
              with:
                  node-version: '20'

            - name: Checkout repository
              uses: actions/checkout@v6
              with:
                  ref: release
                  fetch-depth: 0
                  fetch-tags: 'true'

            - name: Set up Python 3.12
              uses: actions/setup-python@v6
              with:
                  # Semantic version range syntax or exact version of a Python version
                  python-version: '3.12'
                  # Optional - x64 or x86 architecture, defaults to x64
                  architecture: 'x64'

            - name: Get version from Git
              run: |
                  # Get latest version tag number (e.g. main was tagged in GitHub for this Release)
                  FULL_VERSION_NUMBER="$(git describe --tags `git rev-list --tags --max-count=1`)"
                  echo "version_number=$(echo "${FULL_VERSION_NUMBER}" | cut -c2- )" >> $GITHUB_ENV

            - name: configure aws credentials
              uses: aws-actions/configure-aws-credentials@v6
              with:
                  aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
                  aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
                  aws-region: ${{ env.AWS_REGION }}
                  role-session-name: github_to_aws_deploy

            - name: Login to Amazon ECR
              id: login-ecr
              uses: aws-actions/amazon-ecr-login@v2

            - name: Build, tag and push docker images ECR
              env:
                  REGISTRY: ${{ steps.login-ecr.outputs.registry }}
                  IMAGE_TAG: ${{ secrets.IMAGE_TAG }}
                  CLUSTER: ${{ secrets.CLUSTER }}
                  TARGET_SERVICE: ${{ secrets.TARGET_SERVICE }}
              run: |
                  docker build -t concordia .
                  docker build -t concordia/importer --file importer/Dockerfile .
                  docker build -t concordia/celerybeat --file celerybeat/Dockerfile .

                  docker tag concordia:latest $REGISTRY/concordia:$version_number
                  docker tag concordia:latest $REGISTRY/concordia:$IMAGE_TAG
                  docker tag concordia/importer:latest $REGISTRY/concordia/importer:$version_number
                  docker tag concordia/importer:latest $REGISTRY/concordia/importer:$IMAGE_TAG
                  docker tag concordia/celerybeat:latest $REGISTRY/concordia/celerybeat:$version_number
                  docker tag concordia/celerybeat:latest $REGISTRY/concordia/celerybeat:$IMAGE_TAG

                  docker push $REGISTRY/concordia:$version_number
                  docker push $REGISTRY/concordia:$IMAGE_TAG
                  docker push $REGISTRY/concordia/importer:$version_number
                  docker push $REGISTRY/concordia/importer:$IMAGE_TAG
                  docker push $REGISTRY/concordia/celerybeat:$version_number
                  docker push $REGISTRY/concordia/celerybeat:$IMAGE_TAG

                  aws ecs update-service --region ${{ env.AWS_REGION }} --force-new-deployment --cluster $CLUSTER --service $TARGET_SERVICE


================================================
FILE: .github/workflows/test-main-deploy.yml
================================================
name: 'Deploy to test'

on:
    workflow_dispatch:

env:
    AWS_REGION: us-east-1

permissions:
    id-token: write
    contents: read

jobs:
    deploy:
        name: Deploy to Test
        runs-on: ubuntu-latest
        environment:
            name: test

        steps:
            - name: configure aws credentials
              uses: aws-actions/configure-aws-credentials@v6
              with:
                  aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
                  aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
                  aws-region: ${{ env.AWS_REGION }}
                  role-session-name: github_to_aws_deploy

            - name: Login to Amazon ECR
              id: login-ecr
              uses: aws-actions/amazon-ecr-login@v2

            - name: Pull, tag and push docker images ECR
              env:
                  REGISTRY: ${{ steps.login-ecr.outputs.registry }}
                  IMAGE_TAG_PULL: ${{ secrets.IMAGE_TAG_PULL }}
                  IMAGE_TAG: ${{ secrets.IMAGE_TAG }}
                  CLUSTER: ${{ secrets.CLUSTER }}
                  TARGET_SERVICE: ${{ secrets.TARGET_SERVICE }}
                  TARGET_SERVICE_B: ${{ secrets.TARGET_SERVICE_B }}
              run: |
                  docker pull $REGISTRY/concordia:$IMAGE_TAG_PULL
                  docker pull $REGISTRY/concordia/importer:$IMAGE_TAG_PULL
                  docker pull $REGISTRY/concordia/celerybeat:$IMAGE_TAG_PULL

                  docker tag $REGISTRY/concordia:$IMAGE_TAG_PULL $REGISTRY/concordia:$IMAGE_TAG
                  docker tag $REGISTRY/concordia/importer:$IMAGE_TAG_PULL $REGISTRY/concordia/importer:$IMAGE_TAG
                  docker tag $REGISTRY/concordia/celerybeat:$IMAGE_TAG_PULL $REGISTRY/concordia/celerybeat:$IMAGE_TAG

                  docker push $REGISTRY/concordia:$IMAGE_TAG
                  docker push $REGISTRY/concordia/importer:$IMAGE_TAG
                  docker push $REGISTRY/concordia/celerybeat:$IMAGE_TAG

                  aws ecs update-service --region ${{ env.AWS_REGION }} --force-new-deployment --cluster $CLUSTER --service $TARGET_SERVICE
                  aws ecs update-service --region ${{ env.AWS_REGION }} --force-new-deployment --cluster $CLUSTER --service $TARGET_SERVICE_B


================================================
FILE: .github/workflows/test.yml
================================================
name: Test

on:
    workflow_dispatch:
    pull_request:
        branches: [main, 'feature-*', release]
        paths-ignore:
            - docs/**
            - README.md
            - .github/**
            - cloudformation/**
            - db_scripts/**
            - jenkins/**
            - search-proxy/**
            - postgresql/**

env:
    PIPENV_IGNORE_VIRTUALENVS: 1
    DJANGO_SETTINGS_MODULE: concordia.settings_test

jobs:
    test:
        runs-on: ubuntu-latest

        services:
            # Label used to access the service container
            postgres:
                # Docker Hub image
                image: postgres
                # Provide the password for postgres
                env:
                    POSTGRES_DB: concordia
                    POSTGRES_PASSWORD: postgres
                # Set health checks to wait until postgres has started
                options: >-
                    --health-cmd pg_isready
                    --health-interval 10s
                    --health-timeout 5s
                    --health-retries 5
                ports:
                    # Maps tcp port 5432 on service container to the host
                    - 5432:5432

        steps:
            - name: Remove Firefox
              run: sudo apt-get purge firefox

            - name: Install system packages
              run: |
                  sudo apt-get update -qy && sudo apt-get dist-upgrade -qy && sudo apt-get install -qy \
                  libmemcached-dev libz-dev libfreetype6-dev libtiff-dev \
                  libjpeg-dev libopenjp2-7-dev libwebp-dev zlib1g-dev libpq-dev \
                  tesseract-ocr tesseract-ocr-all

            - name: Install node and npm
              uses: actions/setup-node@v6
              with:
                  node-version: '20'

            - name: Checkout repository
              uses: actions/checkout@v6

            - name: Set up Python 3.12
              uses: actions/setup-python@v6
              with:
                  python-version: '3.12'
                  architecture: 'x64'
                  cache: 'pipenv'

            - name: Display Python version
              run: python -c "import sys; print(sys.version)"

            - name: Install Python Dependencies
              run: |
                  python3 -m pip install --upgrade pip
                  pip3 install -U packaging
                  pip3 install -U setuptools
                  pip3 install -U pipenv
                  pipenv install --dev --deploy
                  pipenv install tblib # For parallel test debugging

            - name: Install Node Dependencies #and Add .bin to Path
              run:
                  npm install
                  # echo "PATH=$PWD/node_modules/.bin:$PATH" >> $GITHUB_ENV

            - name: Configure Logs
              run: |
                  mkdir logs
                  touch ./logs/concordia-celery.log

            - name: Bundle, Build (Vite) and Collect Static Files
              run: |
                  npm run build
                  pipenv run ./manage.py collectstatic --no-input --no-post-process

            # - name: Install Chrome for Testing and Set Path
            #   run: |
            #       chromepath=$(npx @puppeteer/browsers install chrome@latest)
            #       chromepath=${chromepath#* }
            #       echo "Chrome installed at: $chromepath"
            #       $chromepath --version
            #       chromepath=${chromepath%/chrome} # Remove the binary so we can add it to the PATH
            #       # Update PATH for subsequent steps
            #       echo "PATH=$chromepath:$PATH" >> $GITHUB_ENV

            - name: Run Tests with Coverage
              run: |
                  mkdir -p coverage_report
                  pipenv run coverage run --parallel-mode ./manage.py test --parallel auto
                  pipenv run coverage combine  # Merge results from parallel test workers
                  # Save full report to coverage_report/coverage.txt and just the total coverage percent to pr_coverage.txt
                  pipenv run coverage report | tee coverage_report/coverage.txt | grep 'TOTAL' | awk '{print $6}' > pr_coverage.txt
                  echo "Stored PR coverage:"
                  cat pr_coverage.txt  # Debugging output to verify correct storage
                  pipenv run coverage html
                  mv htmlcov coverage_report/html  # Move HTML report into a separate directory
              env:
                  PGPASSWORD: postgres
                  # The hostname used to communicate with the PostgreSQL service container
                  POSTGRES_HOST: localhost
                  # The default PostgreSQL port
                  POSTGRES_PORT: 5432
                  # COMMIT_RANGE: ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}

            # Store coverage results if running on the release branch
            - name: Store Release Coverage (if running on release branch)
              if: github.ref == 'refs/heads/release'
              run: cp pr_coverage.txt coverage.txt

            # Cache coverage results if running on the release branch
            - name: Cache Release Coverage (if running on release branch)
              if: github.ref == 'refs/heads/release'
              uses: actions/cache@v5
              with:
                  path: coverage.txt
                  key: release-coverage

            # Upload full coverage report as an artifact
            - name: Upload Full Coverage Report
              uses: actions/upload-artifact@v7
              with:
                  name: coverage-report
                  path: coverage_report

            # Download the stored release branch coverage for PR comparison, if it exists
            - name: Restore Release Coverage (if running on PR)
              if: github.event_name == 'pull_request'
              uses: actions/cache@v5
              with:
                  path: coverage.txt
                  key: release-coverage
                  restore-keys: |
                      release-coverage

            # Compare PR coverage against stored release coverage
            - name: Compare Coverage (if running on PR)
              if: github.event_name == 'pull_request'
              run: |
                  echo "Reading PR coverage from pr_coverage.txt..."
                  cat pr_coverage.txt || echo "⚠️ ERROR: pr_coverage.txt not found or empty"
                  PR_COVERAGE=$(cat pr_coverage.txt)
                  if [ -z "$PR_COVERAGE" ]; then
                      echo "⚠️ ERROR: PR_COVERAGE is empty!"
                      PR_COVERAGE="N/A"
                  fi

                  echo "PR Coverage: $PR_COVERAGE"
                  if [ -f "coverage.txt" ]; then
                      RELEASE_COVERAGE=$(cat coverage.txt)
                      COMPARISON_AVAILABLE=true
                  else
                      COMPARISON_AVAILABLE=false
                      RELEASE_COVERAGE="N/A"
                  fi

                  if [ "$COMPARISON_AVAILABLE" = true ]; then
                      # Strip '%' from PR_COVERAGE and RELEASE_COVERAGE for numerical comparison
                      PR_COVERAGE_NUM=${PR_COVERAGE%\%}
                      RELEASE_COVERAGE_NUM=${RELEASE_COVERAGE%\%}
                      if (( $(echo "$PR_COVERAGE_NUM > $RELEASE_COVERAGE_NUM" | bc -l) )); then
                          CHANGE="🔼 Coverage increased (+$(echo "$PR_COVERAGE_NUM - $RELEASE_COVERAGE_NUM" | bc -l)%)!"
                      elif (( $(echo "$PR_COVERAGE_NUM < $RELEASE_COVERAGE_NUM" | bc -l) )); then
                          CHANGE="🔽 Coverage decreased (-$(echo "$RELEASE_COVERAGE_NUM - $PR_COVERAGE_NUM" | bc -l)%)!"
                      else
                          CHANGE="✅ Coverage remained the same."
                      fi
                  else
                      CHANGE="⚠️ No baseline coverage available from 'release' branch."
                  fi

                  echo "COVERAGE_CHANGE=$CHANGE" >> $GITHUB_ENV
                  printf "RELEASE_COVERAGE=%s\n" "$RELEASE_COVERAGE" >> $GITHUB_ENV
                  printf "PR_COVERAGE=%s\n" "$PR_COVERAGE" >> $GITHUB_ENV

            # Generate and store command for display on the Action UI and PR (if any)
            - name: Generate Coverage Report Comment
              run: |
                  echo "**🛡 Test Coverage Report 🛡**" > coverage_comment.txt
                  echo "- **Current PR Coverage:** ${{ env.PR_COVERAGE }}" >> coverage_comment.txt
                  echo "- **Release Branch Coverage:** ${{ env.RELEASE_COVERAGE }}" >> coverage_comment.txt
                  echo "- **${{ env.COVERAGE_CHANGE }}**" >> coverage_comment.txt
                  echo "- 📊 **[Download Full Coverage Report (Under "Artifacts")](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}#artifacts)**" >> coverage_comment.txt
                  echo "" >> coverage_comment.txt
                  echo "<details>" >> coverage_comment.txt
                  echo "<summary>📜 Click to view full text coverage report</summary>" >> coverage_comment.txt
                  echo "" >> coverage_comment.txt
                  echo '```text' >> coverage_comment.txt
                  cat coverage_report/coverage.txt >> coverage_comment.txt
                  echo '```' >> coverage_comment.txt
                  echo "</details>" >> coverage_comment.txt

            # Display the coverage summary in the GitHub Actions UI
            - name: Post Coverage Summary
              run: cat coverage_comment.txt >> $GITHUB_STEP_SUMMARY

            # Post a comment on the PR with the coverage results
            - name: Comment Coverage Change on PR
              if: github.event_name == 'pull_request'
              uses: mshick/add-pr-comment@v3
              with:
                  message-path: coverage_comment.txt


================================================
FILE: .gitignore
================================================
node_modules/
bin/
target/
local/
build/
.project
.classpath
.settings/
*.pyc
buildstatus.log
deploystatus.log
.metadata/
artifacts/
/.*
!.gitignore
!.cfnlintrc.yaml
!.github
!.dockerignore
.DS_Store
docs/build
env.ini
.venv
*.sqlite3
*.egg-info/
/temp/
/emails/
/logs/*
env-dev.ini
docs/_build
docs/modules
dist/
profile_pics/
mss*
*.swp
config-optional-override.json
env/
concordia/settings_dev_*.py
concordia/settings_test_*.py
concordia/settings_loadtest_*.py
version.txt
static-files


================================================
FILE: Dockerfile
================================================
# Base runtime: Debian 12 (bookworm) slim + Python 3.12.
FROM python:3.12-slim-bookworm

# Major Node.js version to install (e.g., 20, 22). This is used to select the
# NodeSource APT repository "node_<major>.x".
ARG NODE_MAJOR=20

# Include a small "wait for dependencies" helper used by the container command.
# This is downloaded at build time and placed at /wait.
## Add the wait script to the image
ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.2.1/wait /wait
RUN chmod +x /wait

# Prevent interactive prompts during apt operations.
ENV DEBIAN_FRONTEND="noninteractive"

# Bootstrap minimal tooling needed later in the build:
# - curl: download files/keys
# - ca-certificates: validate HTTPS endpoints
# - gnupg: import and dearmor APT repository signing keys
RUN apt-get update -qy && apt-get install -qy curl ca-certificates gnupg

# Trust the Library's certificate authority so the HTTPS tampering proxy does
# not break TLS validation for clients inside the container.
#
# This downloads the CA certificate, converts it to PEM, and refreshes the
# OpenSSL certificate hashes so it is recognized by OpenSSL-based clients.
# Ensure that the Library's certificate authority is trusted so the tampering
# proxy will not break TLS validation. See
# https://staff.loc.gov/wikis/display/SE/Configuring+HTTPS+clients+for+the+HTTPS+tampering+proxy.
RUN curl -fso /etc/ssl/certs/LOC-ROOT-CA-1.crt http://crl.loc.gov/LOC-ROOT-CA-1.crt && openssl x509 -inform der -in /etc/ssl/certs/LOC-ROOT-CA-1.crt -outform pem -out /etc/ssl/certs/LOC-ROOT-CA-1.pem && c_rehash

# Install Node.js via the NodeSource APT repository (manual setup; no setup
# script). Debian bookworm ships Node 18; adding this repo allows installing a
# newer major version (e.g., Node 20) via apt.
#
# This step:
# - creates a dedicated keyring directory under /etc/apt/keyrings
# - downloads and installs the NodeSource signing key into a keyring file
# - registers the NodeSource repository for the selected Node.js major line
#
# Note: When installing Node.js from NodeSource, the `nodejs` package includes
# npm (and npm comes with node-gyp), so there is no separate `npm` or
# `node-gyp` APT package to install here.
#
# References: NodeSource "Repository Manual Installation" guide. https://github.com/nodesource/distributions/wiki/Repository-Manual-Installation
RUN \
    # Create a dedicated directory for third-party APT keyrings.
    mkdir -p /etc/apt/keyrings && \
    # Download the NodeSource repository signing key and store it as a keyring
    # file that apt can use to verify NodeSource packages.
    curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
        | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
    # Register the NodeSource repository for the selected Node.js major version.
    # The "signed-by=" option scopes trust to just this repository entry.
    echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" \
        > /etc/apt/sources.list.d/nodesource.list

# Bring the base OS packages fully up to date, then install system dependencies
# needed to build and run the application.
#
# Notes:
# - dist-upgrade pulls in security and point-release updates for the base image.
# - --force-confnew ensures updated config files are accepted when prompted.
# - autoremove/autoclean reduce image size after installing packages.
RUN apt-get update -qy && apt-get dist-upgrade -qy && apt-get install -o Dpkg::Options::='--force-confnew' -qy \
    build-essential \
    git \
    libmemcached-dev \
    # Pillow/Imaging: https://pillow.readthedocs.io/en/latest/installation.html#external-libraries
    libz-dev libfreetype6-dev \
    libtiff-dev libjpeg-dev libopenjp2-7-dev libwebp-dev zlib1g-dev \
    # Postgres client library to build psycopg
    libpq-dev \
    locales \
    # Weasyprint requirements
    libpango-1.0-0 libharfbuzz0b libpangoft2-1.0-0 \
    # Tesseract
    tesseract-ocr tesseract-ocr-all \
    # Node.js runtime (from NodeSource) and build tooling for native addons.
    nodejs && apt-get -qy autoremove && apt-get -qy autoclean

# Generate and configure a UTF-8 locale for consistent string handling.
RUN locale-gen en_US.UTF-8
ENV LC_ALL=en_US.UTF-8
ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US.UTF-8

# Python runtime settings:
# - unbuffered output for log visibility in containers
# - add /app to PYTHONPATH for module resolution
ENV PYTHONUNBUFFERED=1 \
    PYTHONPATH=/app

# Default Django settings module for container runtime (can be overridden).
ENV DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-concordia.settings_docker}

# Ensure an up-to-date pip and install pipenv for dependency management.
RUN pip install --upgrade pip
RUN pip install --no-cache-dir pipenv

# Copy application code into the image.
WORKDIR /app
COPY . /app

# Front-end build and asset pipeline:
# - update npm to a known major version
# - Install all JS dependencies (including devDependencies for plugins)
RUN npm install --silent --global npm@10 && npm install --silent

# Additional JS build step for Vite.
# - Build assets (Vite) complile scss, bundle, hash and compress js
# - This populates concordia/static/dist with hashed and compressed files.
RUN npm run build

# Create Log Directory
# - Required for Django logging initialization when running collecstatic.
RUN mkdir -p /app/logs

# Install Python dependencies into the system environment using Pipenv and
# - Bake static files into the image (Fast, no post-processing)
# - remove Pipenv cache to reduce image size.
RUN pipenv install --system --dev --deploy && \
    python manage.py collectstatic --no-input --no-post-process && \
    rm -rf ~/.cache/

# - Clean up node artifacts to reduce image size
RUN rm -rf node_modules && rm -rf ~/.cache/

# Container listens on port 80.
EXPOSE 80

# Wait for dependencies (via /wait) and then run the application entrypoint.
CMD /wait && /bin/bash entrypoint.sh


================================================
FILE: LICENSE.md
================================================
As a work of the United States Government, this project is in the
public domain within the United States.

Additionally, we waive copyright and related rights in the work
worldwide through the CC0 1.0 Universal public domain dedication.

## CC0 1.0 Universal Summary

This is a human-readable summary of the
[Legal Code (read the full text)](https://creativecommons.org/publicdomain/zero/1.0/legalcode).

### No Copyright

The person who associated a work with this deed has dedicated the work to
the public domain by waiving all of his or her rights to the work worldwide
under copyright law, including all related and neighboring rights, to the
extent allowed by law.

You can copy, modify, distribute and perform the work, even for commercial
purposes, all without asking permission.

### Other Information

In no way are the patent or trademark rights of any person affected by CC0,
nor are the rights that other persons may have in the work or in how the
work is used, such as publicity or privacy rights.

Unless expressly stated otherwise, the person who associated a work with
this deed makes no warranties about the work, and disclaims liability for
all uses of the work, to the fullest extent permitted by applicable law.
When using or citing the work, you should not imply endorsement by the
author or the affirmer.


================================================
FILE: Loadtesting.md
================================================
# Load Testing Mode

This document describes the current (incomplete but runnable) "load testing mode"
implementation and how to run it end-to-end manually.

Load testing mode consists of:

-   A fixture generator that builds a single JSON fixture from an existing DB
-   A DB preparation command that creates a fresh load test DB, migrates it, and
    loads the fixture while suppressing all Django signals
-   A load test settings file that points the app at the load test DB and disables
    Turnstile blocking
-   A Locust script (`locustfile.py`) plus a wrapper shell script (`load_test.sh`)
    to run a headless load test

The intended lifecycle is:

1. Generate a fixture from a DB with real-ish data
2. Create and populate a fresh `concordia_lt` database from that fixture
3. Run the web app against `concordia_lt` using load test settings
4. Run Locust against that host

The load test database is intended to be single-use.

## Files

-   `concordia/management/commands/create_load_test_fixtures.py`
-   `concordia/management/commands/prepare_load_test_db.py`
-   `concordia/settings_loadtest.py` (or your own `concordia/settings_loadtest_<name>.py`)
-   `locustfile.py` (repo root)
-   `load_test.sh` (repo root)

## Safety notes

-   `create_load_test_fixtures` is read-only against the source DB and only
    writes a JSON file. It is safe to run against production, though it is
    normally run against a refreshed copy of production.
-   `prepare_load_test_db` creates and optionally drops a separate database
    (`concordia_lt`), runs migrations, and loads fixtures into it.
    -   It requires PostgreSQL credentials with `CREATE DATABASE` privileges.
    -   If recreating or dropping, it terminates active connections to the target DB.
-   During fixture load, all Django signals are suppressed to avoid side effects
    (Celery tasks, storage writes, cache updates, derived fields, etc).
-   Storage in load test mode is configured to use dev/staging buckets for safety.
    The workflow is designed to avoid writes to external systems.
-   Locust defaults to a non-production host to reduce risk.

## Prerequisites

-   VPN access to the target environment
-   PostgreSQL credentials available via environment variables
    -   The DB user must be able to connect to `dbname=postgres` and create databases
-   Python environment with the normal dev dependencies installed (Locust is a dev
    dependency)
-   Ability to restart the web app with a different settings module
-   A reachable host running the app in load test mode

## Fixture contents

The fixture generated by `create_load_test_fixtures` contains:

-   Up to 2 published Topics, chosen by ascending `ordering`
-   Up to 5 published Campaigns, preferring Topic-linked Campaigns and filling with
    additional published Campaigns by ascending `ordering`
-   Up to `--assets-limit` Assets (default 10,000), collected by walking:
    -   Topic-linked Projects first, then
    -   Campaign-linked Projects if needed
-   Closure of referenced Items, Projects, Campaigns and Topics for the chosen Assets
-   All Transcriptions for selected Assets
-   Anonymized fixtures for any Users referenced by those Transcriptions
    (`user` and `reviewed_by`)
-   A synthetic pool of test users:
    -   Default: 10,000 users named `locusttest00001`..`locusttest10000`
    -   All share the same password: `locustpass123`
    -   Email: `<username>@example.test`
    -   Users are created with explicit PKs beyond the existing fixture user PKs to
        avoid collisions
-   ProjectTopic rows for selected Topic+Project links (preserves the M2M)

Notes:

-   Selection is best-effort. If there are fewer than `--assets-limit` Assets, the
    fixture is still written.
-   The output is a single JSON file (default `loadtest_fixture.json`).
-   By default, the command validates the fixture by calling `prepare_load_test_db`
    unless `--no-validate` is provided.

## Commands

### 1) Create the fixture

Run against a DB with real data (usually a refreshed prod copy):

```bash
python manage.py create_load_test_fixtures
```

Common options:

```bash
python manage.py create_load_test_fixtures \
  --assets-limit 10000 \
  --test-users 10000 \
  --test-user-prefix locusttest \
  --test-user-password locustpass123 \
  --output loadtest_fixture.json
```

Validation options:

-   `--no-validate` to skip validation
-   `--validate-db-name NAME` to override the validation DB name
-   `--validate-recreate` to recreate the validation DB if it exists
-   `--validate-drop` to drop the validation DB after loading

### 2) Create and populate the load test DB

Standard DB name: `concordia_lt`

```bash
python manage.py prepare_load_test_db \
  --db-name concordia_lt \
  --recreate \
  --fixtures loadtest_fixture.json
```

Behavior:

-   Creates or recreates `concordia_lt`
-   Runs migrations
-   Loads fixtures with all signals suppressed by default

## Running the app in load test mode

### Settings file

`concordia/settings_loadtest.py` is an override layer on top of
`settings_template.py`. It:

-   Points the DB at `concordia_lt`
-   Disables rate limiting
-   Forces Turnstile to always-pass test keys by default
-   Uses console email backend
-   Uses dev buckets for safety
-   Adjusts logging to be visible in common run contexts

If you need a different DB name, do not edit `settings_loadtest.py` directly.
Create a personal settings file, following the local dev convention:

-   `concordia/settings_loadtest_<username>.py`
-   Override `DATABASES["default"]["NAME"]` (and any other local overrides)

### Selecting settings at runtime

Local example:

```bash
DJANGO_SETTINGS_MODULE=concordia.settings_loadtest \
  python manage.py runserver 0.0.0.0:8000
```

Server/container example:

-   Set `DJANGO_SETTINGS_MODULE=concordia.settings_loadtest`
-   Restart/redeploy the web process so it actually uses the load test settings

Important:

-   Creating `concordia_lt` does not affect any running web process.
    You must restart the app with the load test settings selected.

## Locust

### Overview

The load test simulates three flows:

-   Anonymous browsing/transcription page interactions
-   Authenticated users who transcribe
-   Authenticated users who review

The script uses these endpoints:

-   `/` (homepage)
-   `/next-transcribable-asset/` (redirect to next asset)
-   `/next-reviewable-asset/` (redirect to next reviewable asset)
-   `/account/login/` (login)
-   `/account/ajax-status/` and `/account/ajax-messages/` (simulates normal page load)

The script parses asset pages to find:

-   The transcription form action (`<form id="transcription-editor" ...>`)
-   Reservation endpoint (`<script id="asset-reservation-data" data-reserve-asset-url="...">`)
-   Review endpoints (`data-review-url`, `data-submit-url`)

If parsing fails, it is treated as a fundamental mismatch between the Locust
script and the UI.

### "No work" abort behavior

The Locust run aborts the entire test if it determines there is no work
available. "No work" is defined as either:

-   A `/next-*` redirect eventually landing on `/` (homepage), or
-   An asset page not containing the transcription form

This is controlled by:

-   `ABORT_WHEN_NO_WORK = True` (default)
-   `NO_WORK_DUMP_HTML = False` (set True to dump a debug HTML file on abort)

The abort is coordinated across master/workers in distributed mode via a custom
message (`global-abort`). Locust is forced to exit with a non-zero exit code.

### load_test.sh

`load_test.sh` runs Locust in headless mode with defaults that can be overridden
via environment variables.

Defaults:

-   Users: 100
-   Spawn rate: 2
-   Run time: 1m30s
-   Host: `https://crowd-dev.loc.gov`

Override example:

```bash
LOCUST_USERS=500 \
LOCUST_SPAWN_RATE=10 \
LOCUST_RUN_TIME=10m \
LOCUST_HOST=https://your-loadtest-host.example \
./load_test.sh
```

## End-to-end manual runbook

This is the current manual process. Nothing here is automated end-to-end yet.

1. Choose the environment to test

-   Typically your personal environment, dev or staging prepared from a refreshed DB copy of production

2. Generate a fixture

```bash
python manage.py create_load_test_fixtures \
  --output loadtest_fixture.json
```

If you want a smaller dataset for quicker iteration, lower `--assets-limit`
and/or `--test-users`.

3. Create and populate the load test DB

```bash
python manage.py prepare_load_test_db \
  --db-name concordia_lt \
  --recreate \
  --fixtures loadtest_fixture.json
```

4. Switch the web app to load test settings and restart it

-   Set `DJANGO_SETTINGS_MODULE=concordia.settings_loadtest`
-   Restart/redeploy the web process so it uses:
    -   `DATABASES["default"]["NAME"] = "concordia_lt"`
    -   Turnstile always-pass test keys

Sanity checks:

-   Visit the site and confirm pages load without Turnstile blocking.
-   Attempt login with a known test user:
    -   Username: `locusttest00001`
    -   Password: `locustpass123`

5. Run Locust

```bash
./load_test.sh
```

Tune parameters if needed:

```bash
LOCUST_USERS=200 LOCUST_SPAWN_RATE=5 LOCUST_RUN_TIME=5m ./load_test.sh
```

6. Common failure modes

-   Immediate login failures:
    -   App not pointing at `concordia_lt`
    -   Fixture not loaded or test users missing
    -   Turnstile not disabled for load test mode
-   Global abort "no work":
    -   `next-*` redirects to `/` because there is no eligible work
    -   This is likely due to running the script multiple times without refreshing DB
-   Lots of 403s:
    -   Turnstile still active
    -   CSRF issues (the script attempts to seed and use CSRF correctly)

7. Cleanup

There is no automated cleanup step. The DB is intended to be thrown away or
recreated for each run.

To recreate on the next run, rerun step (3) with `--recreate`.

## Known gaps / Next development priorities

-   No single "one command" workflow; all steps are manual.
-   No automated mechanism to build and deploy a load-test-mode container in AWS.
-   No automated environment switching between normal and load test settings.
-   No automated teardown of the load test DB after a run.


================================================
FILE: MANIFEST.in
================================================
include README.md
include MANIFEST.in
recursive-include concordia *
recursive-include tests *.py


================================================
FILE: Makefile
================================================
.PHONY: allup firstup adminuser devup down clean

firstup:
	docker-compose -f docker-compose.yml up -d
	adminuser

adminuser:
	docker-compose -f docker-compose.yml run --rm app ./manage.py shell -c "from django.contrib.auth.models import User; User.objects.create_superuser('admin', 'crowd@loc.gov', '${CONCORDIA_ADMIN_PW}')"

allup:
	docker-compose -f docker-compose.yml up -d

devup:
	docker-compose -f docker-compose.yml up -d

down:
	docker-compose -f docker-compose.yml down

clean:	down
	docker-compose -f docker-compose.yml down -v --remove-orphans
	rm -rf postgresql-data/


================================================
FILE: Pipfile
================================================
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
gunicorn = "==23.0.0"
celery = { extras = ["redis"], version = "==5.5.3" }
django-tinymce = "==4.1.0"
whitenoise = "==6.9.0"
openpyxl = "==3.1.5"
markdown = "==3.10"
django-bootstrap5 = "==25.2"
django-robots = "==6.1"
setuptools-scm = "==9.2.2"
django-ratelimit = "==4.1.0"
pylibmc = "==1.6.3"
kombu = "==5.5.4"
django-flags = "==5.0.14"
sentry-sdk = "==2.57.0"
channels = { extras = ["daphne"], version = "==4.2.2" }
channels-redis = "==4.3.0"
more-itertools = "==10.7.0"
nh3 = "==0.3.4"
django-admin-multiple-choice-list-filter = "==0.1.1"
django-npm = "==1.0.1"
pymemcache = "==4.0.0"
weasyprint = "==68.1"
tesseract = "==0.1.3"
pytesseract = "==0.3.13"
django-redis = "==6.0.0"
twisted = { extras = ["http2", "tls"], version = "==25.5.0" }
pyleniumio = "==1.21.0"
django-maintenance-mode = "==0.22.0"
xlsxwriter = "==3.2.5"
psycopg2 = "==2.9.11"
django-storages = { extras = ["s3"], version = "==1.14.6" }
django-structlog = {extras = ["celery"], version = "==10.0.0"}
defusedxml = "==0.7.1"
django-ninja = "==1.4.3"
urllib3 = "==2.6.3"
bagit = "==1.9.0"
django-registration = "==3.4"
boto3 = "==1.39.17"
botocore = "==1.39.17"
certifi = "==2025.7.14"
websocket-client = "<1.8.0"
black = "*"
django-vite = "~=3.1.0"
pyasn1 = "~=0.6.3"
requests = "~=2.33.0"
hiredis = "~=3.3.0"
django-celery-beat = "~=2.8.1"
prometheus-client = "~=0.25.0"
aws-xray-sdk = "~=2.15.0"
pre-commit = "~=4.5.1"
django = "~=5.2.13"
django-opensearch-dsl = "==0.8.0"

[dev-packages]
invoke = "==2.2.0"
django-extensions = "==3.2.3"
django-debug-toolbar = "==6.3.0"
coverage = "==7.9.2"
locust = "~=2.43"
tblib = "~=3.2.0"
pre-commit = "~=4.5.1"

[requires]
python_version = "3.12"


================================================
FILE: README.md
================================================
[![Lint](https://github.com/LibraryOfCongress/concordia/actions/workflows/black.yml/badge.svg)](https://github.com/LibraryOfCongress/concordia/actions/workflows/black.yml)
[![Test](https://github.com/LibraryOfCongress/concordia/actions/workflows/test.yml/badge.svg)](https://github.com/LibraryOfCongress/concordia/actions/workflows/test.yml)
[![Build](https://github.com/LibraryOfCongress/concordia/actions/workflows/build.yml/badge.svg)](https://github.com/LibraryOfCongress/concordia/actions/workflows/build.yml)
[![Coverage Status](https://coveralls.io/repos/github/LibraryOfCongress/concordia/badge.svg?branch=main)](https://coveralls.io/github/LibraryOfCongress/concordia?branch=main)

# Welcome to Concordia

Concordia is a platform developed by the Library of Congress (LOC) for crowdsourcing transcription and tagging of text in digitized images with the dual goals of collection enhancement and public engagement. Concordia is a user-centered project centering the principles of trust and approachability. [Read our full design principles here](https://github.com/LibraryOfCongress/concordia/blob/master/docs/design-principles.md). Learn more about the Concordia development process in [this Code4Lib article](https://journal.code4lib.org/articles/14901).

LOC launched the first iteration of Concordia as [By the People at crowd.loc.gov](https://crowd.loc.gov/) in October 2018.

The Library of Congress publishes transcriptions created by By the People volunteers on [loc.gov](https://www.loc.gov/) to improve search, readability, and access to handwritten and typed documents. Individual transcriptions are published alongside the transcribed images in digital collections and transcriptions are also published in bulk as [datasets](https://www.loc.gov/search/?fa=contributor:by+the+people+%28program%29). [Learn more about how we publish transcriptions](https://blogs.loc.gov/folklife/2022/05/etl-searching-the-lomax-family-papers-through-the-magic-of-crowdsourcing/).

Concordia code and the By the People transcriptions are released into the public domain. Anyone is free to use or reuse the data. [More info on our licensing page](https://github.com/LibraryOfCongress/concordia/blob/main/LICENSE.md).

As of May 2022 the Library of Congress Concordia development team has moved issues out of Github to an internal system due to reporting needs. Open github issue tickets may not be active or up-to-date. We continue to publish our code here as it is released. Learn more about [How We Work](https://github.com/LibraryOfCongress/concordia/blob/main/docs/how-we-work.md).

_Concordia and By the People are supported by the National Digital Library Trust Fund._

## What Concordia does

The application invites volunteers to transcribe and tag digitized images of manuscript and typed materials from the Library’s collections. All transcriptions are made by volunteers and reviewed by volunteers. It takes at least one volunteer to transcribe a page and at least one other volunteer to review and mark it complete. Some complex documents may pass through both transcription and review many times before they are accepted as complete by a volunteer.

Concordia is a containerized Python-Django-Postgres-etc web application. The Library hosts its instance in the cloud.

Concordia leverages the publicly-available [loc.gov API](https://libraryofcongress.github.io/data-exploration/) to call collection metadata and images in JPEG format and save copies for use in Concordia. Completed transcriptions can be exported out of the application as a single CSV or individual TXT files in a BagIt bag.

## Want to use or reuse our code?

For more on our tech stack and to learn how to set up the Concordia on your computer, check out the [For Developers page](docs/for-developers.md).

## Want to help?

We're excited that you want to be part of Concordia! Here are two ways to contribute:

**1. Report bugs by submitting an issue.** If you are reporting a bug, please include:

-   Your operating system name and version.
-   Any details about your local setup that might be helpful in troubleshooting.
-   Detailed steps to reproduce the bug.

**2. Create an issue to give feedback or suggest a new feature.** The best way to give feedback is to file an issue at https://github.com/LibraryOfCongress/concordia/issues. If you are proposing a feature:

-   Explain in detail how it would work.
-   Explain how it would serve Concordia via a user story
-   Keep the scope as narrow as possible, to make it easier to implement.

If you use or build on our code, we'd love to hear from you! [Contact us here at ask.loc.gov](https://ask.loc.gov/).


================================================
FILE: build_containers.sh
================================================
#!/bin/bash

set -eu -o pipefail

BUILD_ALL=${BUILD_ALL:=0}
BUILD_NUMBER=${BUILD_NUMBER:=1}
TAG=${TAG:-test}
PUBLISH_CONTAINERS=${PUBLISH_CONTAINERS:=1}

# Get an unique venv folder to use inside workspace
VENV=".venv-${BUILD_NUMBER}"

# Initialize new venv
python3 -m venv "${VENV}"
source "${VENV}/bin/activate"

# Update pip
pip3 install -U pip
pip3 install packaging
pip3 install -U setuptools
pip3 install -U pipenv

pipenv install --dev --system --deploy

FULL_VERSION_NUMBER="$(python3 setup.py --version)"
VERSION_NUMBER=$(echo "${FULL_VERSION_NUMBER}" | cut -d '+' -f 1)

if [ $PUBLISH_CONTAINERS -eq 1 ]; then
    AWS_ACCOUNT_ID="$(aws sts get-caller-identity  --output=text --query "Account")"
    eval "$(aws ecr get-login --no-include-email --region us-east-1)"
fi

python3 setup.py build

docker build -t concordia .

if [ $PUBLISH_CONTAINERS -eq 1 ]; then
    docker tag concordia:latest "${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/concordia:${VERSION_NUMBER}"
    docker tag concordia:latest "${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/concordia:${TAG}"
    docker push "${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/concordia:${VERSION_NUMBER}"
    docker push "${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/concordia:${TAG}"
fi

if [ $BUILD_ALL -eq 1 ]; then

    docker build -t concordia/importer --file importer/Dockerfile .
    docker build -t concordia/celerybeat --file celerybeat/Dockerfile .

    if [ $PUBLISH_CONTAINERS -eq 1 ]; then
        docker tag concordia/importer:latest "${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/concordia/importer:${VERSION_NUMBER}"
        docker tag concordia/importer:latest "${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/concordia/importer:${TAG}"
        docker push "${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/concordia/importer:${VERSION_NUMBER}"
        docker push "${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/concordia/importer:${TAG}"

        docker tag concordia/celerybeat:latest "${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/concordia/celerybeat:${VERSION_NUMBER}"
        docker tag concordia/celerybeat:latest "${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/concordia/celerybeat:${TAG}"
        docker push "${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/concordia/celerybeat:${VERSION_NUMBER}"
        docker push "${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/concordia/celerybeat:${TAG}"
    fi
fi


================================================
FILE: celerybeat/Dockerfile
================================================
FROM python:3.12-slim-bookworm

## Add the wait script to the image
ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.2.1/wait /wait
RUN chmod +x /wait

ENV DEBIAN_FRONTEND="noninteractive"

RUN apt-get update -qy && apt-get install -qy curl

# Ensure that the Library's certificate authority is trusted so the tampering
# proxy will not break TLS validation. See
# https://staff.loc.gov/wikis/display/SE/Configuring+HTTPS+clients+for+the+HTTPS+tampering+proxy.

RUN curl -fso /etc/ssl/certs/LOC-ROOT-CA-1.crt http://crl.loc.gov/LOC-ROOT-CA-1.crt && openssl x509 -inform der -in /etc/ssl/certs/LOC-ROOT-CA-1.crt -outform pem -out /etc/ssl/certs/LOC-ROOT-CA-1.pem && c_rehash

RUN apt-get update -qy && apt-get dist-upgrade -qy && apt-get install -o Dpkg::Options::='--force-confnew' -qy \
    git \
    libmemcached-dev \
    # Pillow/Imaging: https://pillow.readthedocs.io/en/latest/installation.html#external-libraries
    libz-dev libfreetype6-dev \
    libtiff-dev libjpeg-dev libopenjp2-7-dev libwebp-dev zlib1g-dev \
    # Postgres client library to build psycopg
    libpq-dev \
    locales \
    # Weasyprint requirements
    libpango-1.0-0 libharfbuzz0b libpangoft2-1.0-0 \
    gcc && apt-get -qy autoremove && apt-get -qy autoclean

RUN locale-gen en_US.UTF-8
ENV LC_ALL=en_US.UTF-8
ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US.UTF-8

ENV PYTHONUNBUFFERED=1 \
    PYTHONPATH=/app

ENV DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-concordia.settings_docker}

RUN pip install --upgrade pip
RUN pip install --no-cache-dir pipenv

WORKDIR /app
COPY . /app

RUN pipenv install --system --dev --deploy && rm -rf ~/.cache/

CMD /wait && ./celerybeat/entrypoint.sh


================================================
FILE: celerybeat/entrypoint.sh
================================================
#!/bin/bash

set -e -u # Exit immediately for unhandled errors or undefined variables

mkdir -p /app/logs
touch /app/logs/concordia.log

#  To avoid trace and reporting of errors in the X-Ray SDK
export AWS_XRAY_CONTEXT_MISSING=LOG_ERROR

echo "Running celerybeat"
celery -A concordia beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler


================================================
FILE: cloudformation/LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "{}"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright 2016 Amazon Web Services, Inc.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: cloudformation/NOTICE
================================================
ecs-refarch-cloudformation
Copyright 2011-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.


================================================
FILE: cloudformation/README.md
================================================
# Note Regarding Concordia Usage

This README, and set of CloudFormation templates, is based on the AWS sample templates at [ecs-refarch-cloudformation](https://s3.amazonaws.com/ecs-refarch-cloudformation/master.yaml).

The sample templates have been modified and new templates have been added.

To use these templates:

1.  Upload this directory to an S3 bucket:

```
cd cloudformation
./sync_templates.sh
```

2.  Read [how to get started with AWS ECR](https://docs.aws.amazon.com/AmazonECR/latest/userguide/ECR_GetStarted.html) and follow the instructions to create an ECR repository for each docker image that will be deployed.
3.  Set a BUILD_NUMBER in your environment and run `./build_containers.sh`
4.  Create a KMS key for this project.
5.  Populate the secrets in `create_secrets.sh` and run that script to create a new set of secrets.
6.  Upload a certificate for the environment to IAM using the canonical host name.
7.  If you don't already have the ECS service linked role in your AWS account, run: `aws iam create-service-linked-role --aws-service-name ecs.amazonaws.com`
8.  Use CloudFormation to create a stack, using the `master.yaml` in the S3 bucket you uploaded in step 1 as the initial template.
9.  If your environment name is not dev, test, stage or prod: Create a new revision of the task definition, changing the ENV_NAME variable to point to the correct secret storage location. Update the service to use the newest task definition version.

![build-status](https://codebuild.eu-west-1.amazonaws.com/badges?uuid=eyJlbmNyeXB0ZWREYXRhIjoiKzBuNjJCUFk2STRvbDZENXlMUFJOenF2V2EyQ3FMbEtuWDlQeVp6TWlxdXhNMGVOZGo5bG9jdTl1YU16RmZIVVNxa3VqTVg3V3drSnJxOUQwSmhqV2g0PSIsIml2UGFyYW1ldGVyU3BlYyI6IlJJRE4wZGJaS25LL0s0dzkiLCJtYXRlcmlhbFNldFNlcmlhbCI6MX0%3D&branch=master)

# Deploying Microservices with Amazon ECS, AWS CloudFormation, and an Application Load Balancer

This reference architecture provides a set of YAML templates for deploying microservices to [Amazon EC2 Container Service (Amazon ECS)](http://docs.aws.amazon.com/AmazonECS/latest/developerguide/Welcome.html) with [AWS CloudFormation](https://aws.amazon.com/cloudformation/).

You can launch this CloudFormation stack in your account:

| AWS Region              | Short name |                                                                                                                                                                                                                                                             |
| ----------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| US East (Ohio)          | us-east-2  | [![cloudformation-launch-button](images/cloudformation-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home?region=us-east-2#/stacks/new?stackName=Production&templateURL=https://s3.amazonaws.com/ecs-refarch-cloudformation/master.yaml) |
| US East (N. Virginia)   | us-east-1  | [![cloudformation-launch-button](images/cloudformation-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new?stackName=Production&templateURL=https://s3.amazonaws.com/ecs-refarch-cloudformation/master.yaml) |
| US West (N. California) | us-west-2  | [![cloudformation-launch-button](images/cloudformation-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/new?stackName=Production&templateURL=https://s3.amazonaws.com/ecs-refarch-cloudformation/master.yaml) |
| US West (Oregon)        | us-west-1  | [![cloudformation-launch-button](images/cloudformation-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home?region=us-west-1#/stacks/new?stackName=Production&templateURL=https://s3.amazonaws.com/ecs-refarch-cloudformation/master.yaml) |

## Overview

![infrastructure-overview](images/architecture-overview.png)

The repository consists of a set of nested templates that deploy the following:

-   A tiered [VPC](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Introduction.html) with public and private subnets, spanning an AWS region.
-   A highly available ECS cluster deployed across two [Availability Zones](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html) in an [Auto Scaling](https://aws.amazon.com/autoscaling/) group and that are AWS SSM enabled.
-   A pair of [NAT gateways](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/vpc-nat-gateway.html) (one in each zone) to handle outbound traffic.
-   Two interconnecting microservices deployed as [ECS services](http://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs_services.html) (website-service and product-service).
-   An [Application Load Balancer (ALB)](https://aws.amazon.com/elasticloadbalancing/applicationloadbalancer/) to the public subnets to handle inbound traffic.
-   ALB path-based routes for each ECS service to route the inbound traffic to the correct service.
-   Centralized container logging with [Amazon CloudWatch Logs](http://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/WhatIsCloudWatchLogs.html).
-   A [Lambda Function](https://docs.aws.amazon.com/lambda/latest/dg/welcome.html) and [Auto Scaling Lifecycle Hook](https://docs.aws.amazon.com/autoscaling/ec2/userguide/lifecycle-hooks.html) to [drain Tasks from your Container Instances](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/container-instance-draining.html) when an Instance is selected for Termination in your Auto Scaling Group.

## Why use AWS CloudFormation with Amazon ECS?

Using CloudFormation to deploy and manage services with ECS has a number of nice benefits over more traditional methods ([AWS CLI](https://aws.amazon.com/cli), scripting, etc.).

#### Infrastructure-as-Code

A template can be used repeatedly to create identical copies of the same stack (or to use as a foundation to start a new stack). Templates are simple YAML- or JSON-formatted text files that can be placed under your normal source control mechanisms, stored in private or public locations such as Amazon S3, and exchanged via email. With CloudFormation, you can see exactly which AWS resources make up a stack. You retain full control and have the ability to modify any of the AWS resources created as part of a stack.

#### Self-documenting

Fed up with outdated documentation on your infrastructure or environments? Still keep manual documentation of IP ranges, security group rules, etc.?

With CloudFormation, your template becomes your documentation. Want to see exactly what you have deployed? Just look at your template. If you keep it in source control, then you can also look back at exactly which changes were made and by whom.

#### Intelligent updating & rollback

CloudFormation not only handles the initial deployment of your infrastructure and environments, but it can also manage the whole lifecycle, including future updates. During updates, you have fine-grained control and visibility over how changes are applied, using functionality such as [change sets](https://aws.amazon.com/blogs/aws/new-change-sets-for-aws-cloudformation/), [rolling update policies](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-updatepolicy.html) and [stack policies](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html).

## Template details

The templates below are included in this repository and reference architecture:

| Template                                                                       | Description                                                                                                                                                                                                                                                                                                                                                                          |
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [master.yaml](master.yaml)                                                     | This is the master template - deploy it to CloudFormation and it includes all of the others automatically.                                                                                                                                                                                                                                                                           |
| [infrastructure/vpc.yaml](infrastructure/vpc.yaml)                             | This template deploys a VPC with a pair of public and private subnets spread across two Availability Zones. It deploys an [Internet gateway](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Internet_Gateway.html), with a default route on the public subnets. It deploys a pair of NAT gateways (one in each zone), and default routes for them in the private subnets. |
| [infrastructure/security-groups.yaml](infrastructure/security-groups.yaml)     | This template contains the [security groups](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_SecurityGroups.html) required by the entire stack. They are created in a separate nested template, so that they can be referenced by all of the other nested templates.                                                                                                       |
| [infrastructure/load-balancers.yaml](infrastructure/load-balancers.yaml)       | This template deploys an ALB to the public subnets, which exposes the various ECS services. It is created in in a separate nested template, so that it can be referenced by all of the other nested templates and so that the various ECS services can register with it.                                                                                                             |
| [infrastructure/ecs-cluster.yaml](infrastructure/ecs-cluster.yaml)             | This template deploys an ECS cluster to the private subnets using an Auto Scaling group and installs the AWS SSM agent with related policy requirements.                                                                                                                                                                                                                             |
| [infrastructure/lifecyclehook.yaml](infrastructure/lifecyclehook.yaml)         | This template deploys a Lambda Function and Auto Scaling Lifecycle Hook to drain Tasks from your Container Instances when an Instance is selected for Termination in your Auto Scaling Group.                                                                                                                                                                                        |
| [services/product-service/service.yaml](services/product-service/service.yaml) | This is an example of a long-running ECS service that serves a JSON API of products. For the full source for the service, see [services/product-service/src](services/product-service/src).                                                                                                                                                                                          |
| [services/website-service/service.yaml](services/website-service/service.yaml) | This is an example of a long-running ECS service that needs to connect to another service (product-service) via the load-balanced URL. We use an environment variable to pass the product-service URL to the containers. For the full source for this service, see [services/website-service/src](services/website-service/src).                                                     |

After the CloudFormation templates have been deployed, the [stack outputs](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html) contain a link to the load-balanced URLs for each of the deployed microservices.

![stack-outputs](images/stack-outputs.png)

The ECS instances should also appear in the Managed Instances section of the EC2 console.

## How do I...?

### Get started and deploy this into my AWS account

You can launch this CloudFormation stack in your account:

| AWS Region              | Short name |                                                                                                                                                                                                                                                             |
| ----------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| US East (Ohio)          | us-east-2  | [![cloudformation-launch-button](images/cloudformation-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home?region=us-east-2#/stacks/new?stackName=Production&templateURL=https://s3.amazonaws.com/ecs-refarch-cloudformation/master.yaml) |
| US East (N. Virginia)   | us-east-1  | [![cloudformation-launch-button](images/cloudformation-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new?stackName=Production&templateURL=https://s3.amazonaws.com/ecs-refarch-cloudformation/master.yaml) |
| US West (N. California) | us-west-2  | [![cloudformation-launch-button](images/cloudformation-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/new?stackName=Production&templateURL=https://s3.amazonaws.com/ecs-refarch-cloudformation/master.yaml) |
| US West (Oregon)        | us-west-1  | [![cloudformation-launch-button](images/cloudformation-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home?region=us-west-1#/stacks/new?stackName=Production&templateURL=https://s3.amazonaws.com/ecs-refarch-cloudformation/master.yaml) |

### Customize the templates

1. [Fork](https://github.com/awslabs/ecs-refarch-cloudformation#fork-destination-box) this GitHub repository.
1. Clone the forked GitHub repository to your local machine.
1. Modify the templates.
1. Verify your changes locally: `pipenv run cfn-lint path/to/template.yaml`
1. Upload them to an Amazon S3 bucket of your choice.
1. Either create a new CloudFormation stack by deploying the master.yaml template, or update your existing stack with your version of the templates.

### Create a new ECS service

1. Push your container to a registry somewhere (e.g., [Amazon ECR](https://aws.amazon.com/ecr/)).
2. Copy one of the existing service templates in [services/\*](/services).
3. Update the `ContainerName` and `Image` parameters to point to your container image instead of the example container.
4. Increment the `ListenerRule` priority number (no two services can have the same priority number - this is used to order the ALB path based routing rules).
5. Copy one of the existing service definitions in [master.yaml](master.yaml) and point it at your new service template. Specify the HTTP `Path` at which you want the service exposed.
6. Deploy the templates as a new stack, or as an update to an existing stack.

### Setup centralized container logging

By default, the containers in your ECS tasks/services are already configured to send log information to CloudWatch Logs and retain them for 365 days. Within each service's template (in [services/\*](services/)), a LogGroup is created that is named after the CloudFormation stack. All container logs are sent to that CloudWatch Logs log group.

You can view the logs by looking in your [CloudWatch Logs console](https://console.aws.amazon.com/cloudwatch/home?#logs:) (make sure you are in the correct AWS region).

ECS also supports other logging drivers, including `syslog`, `journald`, `splunk`, `gelf`, `json-file`, and `fluentd`. To configure those instead, adjust the service template to use the alternative `LogDriver`. You can also adjust the log retention period from the default 365 days by tweaking the `RetentionInDays` parameter.

For more information, see the [LogConfiguration](http://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_LogConfiguration.html) API operation.

### Change the ECS host instance type

This is specified in the [master.yaml](master.yaml) template.

By default, [t2.large](https://aws.amazon.com/ec2/instance-types/) instances are used, but you can change this by modifying the following section:

```
ECS:
  Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ...
      Parameters:
        ...
        InstanceType: t2.large
        InstanceCount: 4
        ...
```

### Adjust the Auto Scaling parameters for ECS hosts and services

The Auto Scaling group scaling policy provided by default launches and maintains a cluster of 4 ECS hosts distributed across two Availability Zones (min: 4, max: 4, desired: 4).

It is **_not_** set up to scale automatically based on any policies (CPU, network, time of day, etc.).

If you would like to configure policy or time-based automatic scaling, you can add the [ScalingPolicy](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-policy.html) property to the AutoScalingGroup deployed in [infrastructure/ecs-cluster.yaml](infrastructure/ecs-cluster.yaml#L69).

As well as configuring Auto Scaling for the ECS hosts (your pool of compute), you can also configure scaling each individual ECS service. This can be useful if you want to run more instances of each container/task depending on the load or time of day (or a custom CloudWatch metric). To do this, you need to create [AWS::ApplicationAutoScaling::ScalingPolicy](http://docs.aws.amazon.com/pt_br/AWSCloudFormation/latest/UserGuide/aws-resource-applicationautoscaling-scalingpolicy.html) within your service template.

### Deploy multiple environments (e.g., dev, test, pre-production)

Deploy another CloudFormation stack from the same set of templates to create a new environment. The stack name provided when deploying the stack is prefixed to all taggable resources (e.g., EC2 instances, VPCs, etc.) so you can distinguish the different environment resources in the AWS Management Console.

### Change the VPC or subnet IP ranges

This set of templates deploys the following network design:

| Item           | CIDR Range     | Usable IPs | Description                                        |
| -------------- | -------------- | ---------- | -------------------------------------------------- |
| VPC            | 10.180.0.0/16  | 65,536     | The whole range used for the VPC and all subnets   |
| Public Subnet  | 10.180.8.0/21  | 2,041      | The public subnet in the first Availability Zone   |
| Public Subnet  | 10.180.16.0/21 | 2,041      | The public subnet in the second Availability Zone  |
| Private Subnet | 10.180.24.0/21 | 2,041      | The private subnet in the first Availability Zone  |
| Private Subnet | 10.180.32.0/21 | 2,041      | The private subnet in the second Availability Zone |

You can adjust the CIDR ranges used in this section of the [master.yaml](master.yaml) template:

```
VPC:
  Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Sub ${TemplateLocation}/infrastructure/vpc.yaml
      Parameters:
        EnvironmentName:    !Ref AWS::StackName
        VpcCIDR:            10.180.0.0/16
        PublicSubnet1CIDR:  10.180.8.0/21
        PublicSubnet2CIDR:  10.180.16.0/21
        PrivateSubnet1CIDR: 10.180.24.0/21
        PrivateSubnet2CIDR: 10.180.32.0/21
```

### Update an ECS service to a new Docker image version

ECS has the ability to perform rolling upgrades to your ECS services to minimize downtime during deployments. For more information, see [Updating a Service](http://docs.aws.amazon.com/AmazonECS/latest/developerguide/update-service.html).

To update one of your services to a new version, adjust the `Image` parameter in the service template (in [services/\*](services/) to point to the new version of your container image. For example, if `1.0.0` was currently deployed and you wanted to update to `1.1.0`, you could update it as follows:

```
TaskDefinition:
  Type: AWS::ECS::TaskDefinition
  Properties:
    ContainerDefinitions:
      - Name: your-container
        Image: registry.example.com/your-container:1.1.0
```

After you've updated the template, update the deployed CloudFormation stack; CloudFormation and ECS handle the rest.

To adjust the rollout parameters (min/max number of tasks/containers to keep in service at any time), you need to configure `DeploymentConfiguration` for the ECS service.

For example:

```
Service:
  Type: AWS::ECS::Service
    Properties:
      ...
      DesiredCount: 4
      DeploymentConfiguration:
        MaximumPercent: 200
        MinimumHealthyPercent: 50
```

### Use the SSM Run Command function to see details in the ECS instances

The AWS SSM Run Command function, in the EC2 console, can be used to execute commands at the shell on the ECS instances. These can be helpful for examining the installed configuration of the instances without requiring direct access to them.

### Spot Instances and the Hibernate Agent.

In order to use Spot with this template, you will need to enable `SpotPrice` under the `AWS::AutoScaling::LaunchConfiguration` or add in `AWS::EC2::SpotFleet` support. To fully use Hibernation with Spot instances, please review [Spot Instance Interruptions](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-interruptions.html).

### Add a new item to this list

If you found yourself wishing this set of frequently asked questions had an answer for a particular problem, please [submit a pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/). The chances are that others will also benefit from having the answer listed here.

## Contributing

Please [create a new GitHub issue](https://github.com/awslabs/ecs-refarch-cloudformation/issues/new) for any feature requests, bugs, or documentation improvements.

Where possible, please also [submit a pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/) for the change.

## License

Copyright 2011-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at

[http://aws.amazon.com/apache2.0/](http://aws.amazon.com/apache2.0/)

or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
​


================================================
FILE: cloudformation/add_cloudflare_ips_to_sgs.py
================================================
#!/usr/bin/env python3
"""
Ensure that every security group tagged with “AllowCloudFlareIngress” has
permissions for every public CloudFlare netblock
"""

import sys

import boto3
import requests
from botocore.exceptions import ClientError

EC2_CLIENT = boto3.client("ec2")

CLOUDFLARE_IPV4 = requests.get(
    "https://www.cloudflare.com/ips-v4", timeout=30
).text.splitlines()
CLOUDFLARE_IPV6 = requests.get(
    "https://www.cloudflare.com/ips-v6", timeout=30
).text.splitlines()


def add_ingess_rules_for_group(sg_id, existing_permissions):
    permissions = {"IpProtocol": "tcp", "FromPort": 443, "ToPort": 443}

    existing_ipv4 = set()
    existing_ipv6 = set()

    for existing in existing_permissions:
        if any(
            permissions[k] != existing[k] for k in ("IpProtocol", "FromPort", "ToPort")
        ):
            continue

        existing_ipv4.update(i["CidrIp"] for i in existing["IpRanges"])
        existing_ipv6.update(i["CidrIpv6"] for i in existing["Ipv6Ranges"])

    ipv4_ranges = [
        {"CidrIp": cidr, "Description": "CloudFlare"}
        for cidr in CLOUDFLARE_IPV4
        if cidr not in existing_ipv4
    ]
    ipv6_ranges = [
        {"CidrIpv6": cidr, "Description": "CloudFlare"}
        for cidr in CLOUDFLARE_IPV6
        if cidr not in existing_ipv6
    ]

    permissions["IpRanges"] = ipv4_ranges
    permissions["Ipv6Ranges"] = ipv6_ranges

    try:
        EC2_CLIENT.authorize_security_group_ingress(
            GroupId=sg_id, IpPermissions=[permissions]
        )
    except ClientError as exc:
        print(f"Unable to add permssions for {sg_id}: {exc}", file=sys.stderr)


def get_security_groups():
    paginator = EC2_CLIENT.get_paginator("describe_security_groups")
    page_iterator = paginator.paginate(
        Filters=[{"Name": "tag-key", "Values": ["AllowCloudFlareIngress"]}]
    )

    for page in page_iterator:
        for sg in page["SecurityGroups"]:
            yield sg["GroupId"], sg["IpPermissions"]


if __name__ == "__main__":
    for security_group_id, existing_permissions in get_security_groups():
        add_ingess_rules_for_group(security_group_id, existing_permissions)


================================================
FILE: cloudformation/create_secrets.sh
================================================
#!/bin/bash

set -eu

# If you create a new set of secrets using a new ENV_NAME here,
# then add the new ENV_NAME option to the list of allowed options in
# master.yaml and infrastructure/fargate-cluster.yaml

export ENV_NAME=cftest2

export DJANGO_SECRET_KEY=
export DB_PASSWORD=
export KMS_KEY_ARN=arn:aws:kms:us-east-1:619333082511:key/d300e73d-9170-4001-933a-37af0bcdb956

aws secretsmanager create-secret --name "crowd/${ENV_NAME}/Django/SecretKey" --kms-key-id "${KMS_KEY_ARN}" --secret-string "{\"DjangoSecretKey\": \"${DJANGO_SECRET_KEY}\"}"

aws secretsmanager create-secret --name "crowd/${ENV_NAME}/DB/MasterUserPassword" --kms-key-id "${KMS_KEY_ARN}" --secret-string "{\"username\": \"concordia\",\"engine\": \"postgres\",\"port\": 5432,\"dbname\": \"concordia\",\"password\": \"${DB_PASSWORD}\"}"

# aws secretsmanager create-secret --name "concordia/SMTP" --kms-key-id "${KMS_KEY_ARN}" --secret-string '{"Hostname": "email-smtp.us-east-1.amazonaws.com","Username": "","Password": ""}'


================================================
FILE: cloudformation/featurebranch.yaml
================================================
---
AWSTemplateFormatVersion: '2010-09-09'
Description: >
    Deploy a feature branch to a subdomain of crowd-test.loc.gov
    using pre-existing infrastructure.
    Assumes docker images have been published to ECR with
    tag matching the feature branch name.

Parameters:
    ConcordiaBranch:
        Description: which branch name to deploy
        Type: String
        Default: release

    AbbreviatedName:
        Description: an abbreviation used for creating short-named cloudformation resources
        Type: String
        Default: rel

    Priority:
        Type: Number
        Description: Priority of the subdomain listener rule, must be unique in the set of listener rules
        Default: 100

Resources:
    RDS:
        Type: AWS::CloudFormation::Stack
        Properties:
            TemplateURL: 'https://s3.amazonaws.com/crowd-deployment/infrastructure/rds.yaml'
            Parameters:
                DbPassword: '{{resolve:secretsmanager:crowd/test/DB/MasterUserPassword:SecretString:password}}'
                DbUsername: '{{resolve:secretsmanager:crowd/test/DB/MasterUserPassword:SecretString:username}}'
                DatabaseSecurityGroup: 'sg-0496910b800de2869'
                PrivateSubnet1: 'subnet-0aa55b322229b945a'
                PrivateSubnet2: 'subnet-0f65558b319b2d4dc'

    DataLoadHost:
        Type: AWS::CloudFormation::Stack
        Properties:
            TemplateURL: 'https://s3.amazonaws.com/crowd-deployment/infrastructure/data-load.yaml'
            Parameters:
                PostgresqlHost: !GetAtt RDS.Outputs.DatabaseHostName
                PostgresqlPassword: '{{resolve:secretsmanager:crowd/test/DB/MasterUserPassword:SecretString:password}}'
                EnvironmentName: 'test'

    ElastiCache:
        Type: AWS::CloudFormation::Stack
        Properties:
            TemplateURL: 'https://s3.amazonaws.com/crowd-deployment/infrastructure/elasticache-feature.yaml'
            Parameters:
                EnvironmentName: !Ref AbbreviatedName
                SecurityGroup: 'sg-028ebfe14211447c4'

    FargateCluster:
        Type: AWS::CloudFormation::Stack
        Properties:
            TemplateURL: 'https://s3.amazonaws.com/crowd-deployment/infrastructure/fargate-featurebranch.yaml'
            Parameters:
                EnvName: 'test'
                FullEnvironmentName: 'test'
                S3BucketName: 'crowd-test-content'
                ExportS3BucketName: 'crowd-test-export'
                ConcordiaVersion: !Ref ConcordiaBranch
                CanonicalHostName: !Sub '${ConcordiaBranch}.crowd-test.loc.gov'
                VpcId: 'vpc-018e5a73079d0b350'
                SecurityGroup: 'sg-04de21574623caca7'
                RedisAddress: !GetAtt ElastiCache.Outputs.RedisAddress
                RedisPort: !GetAtt ElastiCache.Outputs.RedisPort
                DatabaseEndpoint: !GetAtt RDS.Outputs.DatabaseHostName
                Priority: !Ref Priority
                DataLoadStackName: !GetAtt DataLoadHost.Outputs.StackName


================================================
FILE: cloudformation/infrastructure/bastion-hosts.yaml
================================================
Description: This template deploys a bastion host in each of the public subnets.

Parameters:
    EnvironmentName:
        Description: An environment name that will be prefixed to resource names
        Type: String
        AllowedValues:
            - dev
            - test
            - stage
            - prod

    KeyPairName:
        Description: key pair (within this region) for ECS instances access
        Type: String

Mappings:
    AWSRegionToAMI:
        us-east-1:
            AMI: ami-04e5276ebb8451442

    EnvironmentMapping:
        IamInstanceProfileName:
            dev: crowd-dev-FargateCluster-WFCY4I0U7JSM-ConcordiaInstanceProfile-RQHLRZADDM9M
            test: crowd-test-FargateCluster-1R5U1VT4HOYX2-ConcordiaInstanceProfile-1FJXY570ZM2O3
            stage: crowd-stage-FargateCluster-1TBKSIZQKLJHV-ConcordiaInstanceProfile-1XG3TR3LY42ND
            prod: crowd-prod-FargateCluster-1X1CI0J3HFJ9F-ConcordiaInstanceProfile-13SHE5FAB7D6Q

        # The ID of the public subnet in the first AZ
        # Type: AWS::EC2::Subnet::Id
        PublicSubnet1:
            dev: subnet-079b5dd4f9acf44e6
            test: subnet-06f443ea589879e8d
            stage: subnet-06f40e2fc8d891692
            prod: subnet-09fdaf1c5c73f588f

        # The ID of the public subnet in the second AZ
        # Type: AWS::EC2::Subnet::Id
        PublicSubnet2:
            dev: subnet-01d6614725c7dabd6
            test: subnet-05a15c6058ebdf54f
            stage: subnet-0a022eb0c614b0b00
            prod: subnet-01580e2a4d6d42b52

        # The security group for bastion hosts
        # Type: AWS::EC2::SecurityGroup::Id
        BastionHostsSecurityGroup:
            dev: sg-062afe8941ace25ad
            test: sg-0208b0df704b66c3c
            stage: sg-0a2175a2df32a4332
            prod: sg-066c68e77787b2a10

Resources:
    Bastion1:
        Type: AWS::EC2::Instance
        Properties:
            ImageId:
                Fn::FindInMap:
                    - AWSRegionToAMI
                    - Ref: 'AWS::Region'
                    - 'AMI'
            InstanceType: 't2.medium'
            IamInstanceProfile:
                Fn::FindInMap:
                    - EnvironmentMapping
                    - IamInstanceProfileName
                    - Ref: EnvironmentName
            KeyName:
                Ref: KeyPairName
            NetworkInterfaces:
                - AssociatePublicIpAddress: true
                  DeviceIndex: '0'
                  GroupSet:
                      - Fn::FindInMap:
                            - EnvironmentMapping
                            - BastionHostsSecurityGroup
                            - Ref: EnvironmentName
                  SubnetId:
                      Fn::FindInMap:
                          - EnvironmentMapping
                          - PublicSubnet1
                          - Ref: EnvironmentName
            UserData:
                Fn::Base64: !Sub |
                    #!/bin/bash -xe
                    echo "Running userdata for ${EnvironmentName}"
                    echo "export ENV_NAME=${EnvironmentName}" >> /home/ec2-user/.bash_profile
                    source /home/ec2-user/.bash_profile
                    # TODO while true is a workaround for AL2023 Consistently Failing to boot
                    #  · Issue #3741· philips-labs/terraform-aws-github-runner
                    # https://github.com/amazonlinux/amazon-linux-2023/issues/397
                    while true; do
                      dnf -y upgrade --releasever=latest && break
                    done
                    while true; do
                      dnf -y install --assumeyes git && break
                    done
                    while true; do
                      dnf -y install --assumeyes postgresql15.x86_64 && break
                    done
                    while true; do
                      dnf -y install --assumeyes docker.x86_64 && break
                    done
                    aws s3 cp s3://crowd-deployment/database-dumps/concordia.latest.dmp concordia.dmp
            Tags:
                - Key: Name
                  Value: !Sub ${EnvironmentName}-BastionHost-1

    Bastion2:
        Type: AWS::EC2::Instance
        Properties:
            ImageId:
                Fn::FindInMap:
                    - AWSRegionToAMI
                    - Ref: 'AWS::Region'
                    - 'AMI'
            InstanceType: 't2.medium'
            IamInstanceProfile:
                Fn::FindInMap:
                    - EnvironmentMapping
                    - IamInstanceProfileName
                    - Ref: EnvironmentName
            KeyName:
                Ref: KeyPairName
            NetworkInterfaces:
                - AssociatePublicIpAddress: true
                  DeviceIndex: '0'
                  GroupSet:
                      - Fn::FindInMap:
                            - EnvironmentMapping
                            - BastionHostsSecurityGroup
                            - Ref: EnvironmentName
                  SubnetId:
                      Fn::FindInMap:
                          - EnvironmentMapping
                          - PublicSubnet2
                          - Ref: EnvironmentName
            UserData:
                Fn::Base64: !Sub |
                    #!/bin/bash -xe
                    echo "Running userdata for ${EnvironmentName}"
                    echo "export ENV_NAME=${EnvironmentName}" >> /home/ec2-user/.bash_profile
                    source /home/ec2-user/.bash_profile
                    while true; do
                      dnf -y upgrade --releasever=latest && break
                    done
                    while true; do
                      dnf -y install --assumeyes git && break
                    done
                    while true; do
                      dnf -y install --assumeyes postgresql15.x86_64 && break
                    done
                    while true; do
                      dnf -y install --assumeyes docker.x86_64 && break
                    done
                    aws s3 cp s3://crowd-deployment/database-dumps/concordia.latest.dmp concordia.dmp
            Tags:
                - Key: Name
                  Value: !Sub ${EnvironmentName}-BastionHost-2


================================================
FILE: cloudformation/infrastructure/data-load.yaml
================================================
Description:
    This template deploys a host in a private subnet and loads the most recent
    database dump to the specified database server.

Parameters:
    EnvironmentName:
        Description: An environment name that will be prefixed to resource names
        Type: String
        AllowedValues:
            - dev
            - test
            - stage
            - prod

    PostgresqlHost:
        Description: the end point of the RDS database host to restore
        Type: String

    PostgresqlPassword:
        Description: the password for the RDS endpoint to restore
        Type: String
        NoEcho: true

Mappings:
    AWSRegionToAMI:
        us-east-1:
            AMI: ami-04e5276ebb8451442

    EnvironmentMapping:
        IamInstanceProfileName:
            dev: crowd-dev-FargateCluster-WFCY4I0U7JSM-ConcordiaInstanceProfile-RQHLRZADDM9M
            test: crowd-test-FargateCluster-1R5U1VT4HOYX2-ConcordiaInstanceProfile-1FJXY570ZM2O3
            stage: crowd-stage-FargateCluster-1TBKSIZQKLJHV-ConcordiaInstanceProfile-1XG3TR3LY42ND
            prod: crowd-prod-FargateCluster-1X1CI0J3HFJ9F-ConcordiaInstanceProfile-13SHE5FAB7D6Q

        PrivateSubnet1:
            dev: subnet-0c95a830ce007fa65
            test: subnet-0aa55b322229b945a
            stage: subnet-0f7c7d66b66d6dd90
            prod: subnet-0da84976b66c32ce4

        # The security group for bastion hosts
        # Type: AWS::EC2::SecurityGroup::Id
        BastionHostsSecurityGroup:
            dev: sg-062afe8941ace25ad
            test: sg-0208b0df704b66c3c
            stage: sg-0a2175a2df32a4332
            prod: sg-066c68e77787b2a10

Resources:
    DataLoadHost:
        Type: AWS::EC2::Instance
        CreationPolicy:
            ResourceSignal:
                Timeout: PT30M
        Properties:
            ImageId:
                Fn::FindInMap:
                    - AWSRegionToAMI
                    - Ref: 'AWS::Region'
                    - 'AMI'
            InstanceType: 't2.medium'
            IamInstanceProfile:
                Fn::FindInMap:
                    - EnvironmentMapping
                    - IamInstanceProfileName
                    - Ref: EnvironmentName
            InstanceInitiatedShutdownBehavior: terminate
            NetworkInterfaces:
                - AssociatePublicIpAddress: true
                  DeviceIndex: '0'
                  GroupSet:
                      - Fn::FindInMap:
                            - EnvironmentMapping
                            - BastionHostsSecurityGroup
                            - Ref: EnvironmentName
                  SubnetId:
                      Fn::FindInMap:
                          - EnvironmentMapping
                          - PrivateSubnet1
                          - Ref: EnvironmentName
            UserData:
                Fn::Base64: !Sub |
                    #!/bin/bash -xe
                    trap '/opt/aws/bin/cfn-signal --exit-code 1 --resource DataLoadHost --region ${AWS::Region} --stack ${AWS::StackName}' ERR
                    echo "Running userdata for ${EnvironmentName}"
                    echo "export ENV_NAME=${EnvironmentName}" >> /home/ec2-user/.bash_profile
                    source /home/ec2-user/.bash_profile
                    # TODO while true is a workaround for AL2023 Consistently Failing to boot
                    #  · Issue #3741· philips-labs/terraform-aws-github-runner
                    # https://github.com/amazonlinux/amazon-linux-2023/issues/397
                    while true; do
                      dnf -y upgrade --releasever=latest && break
                    done
                    while true; do
                      dnf -y install --assumeyes postgresql15.x86_64 && break
                    done
                    aws s3 cp s3://crowd-deployment/database-dumps/concordia.latest.dmp concordia.dmp
                    echo "${PostgresqlHost}:5432:*:concordia:${PostgresqlPassword}" >> /root/.pgpass
                    chmod 0600 /root/.pgpass
                    psql -U concordia -h ${PostgresqlHost} -d postgres -c "select pg_terminate_backend(pid) from pg_stat_activity where datname='concordia';"
                    psql -U concordia -h ${PostgresqlHost} -d postgres -c "drop database concordia;"
                    pg_restore --create -Fc -U concordia -h ${PostgresqlHost} --dbname=postgres --no-password --no-owner --no-acl concordia.dmp
                    # Signal the status from cfn-init
                    /opt/aws/bin/cfn-signal --exit-code 0 --resource DataLoadHost --region ${AWS::Region} --stack ${AWS::StackName}
                    shutdown -h now
            Tags:
                - Key: Name
                  Value: !Sub ${EnvironmentName}-DataLoadHost
Outputs:
    StackName:
        Description: 'Stackname for the DataLoadHost'
        Value: !Ref AWS::StackName


================================================
FILE: cloudformation/infrastructure/elasticache-feature.yaml
================================================
Description: >
    This template deploys an elasticache cluster to the provided VPC and subnets

Parameters:
    EnvironmentName:
        Description: An environment name that will be prefixed to resource names
        Type: String

    SecurityGroup:
        Description: Select the Security Group to use for the ECS cluster hosts
        Type: AWS::EC2::SecurityGroup::Id

    CacheNodeType:
        Type: String
        Default: cache.m5.large

Resources:
    RedisService:
        Type: AWS::ElastiCache::CacheCluster
        Properties:
            VpcSecurityGroupIds:
                - !Ref 'SecurityGroup'
            CacheSubnetGroupName: 'crowd-cache-1frtjeewr57u7'
            CacheNodeType: !Ref 'CacheNodeType'
            ClusterName: !Sub '${EnvironmentName}-redis'
            Engine: redis
            AutoMinorVersionUpgrade: true
            NumCacheNodes: 1
            SnapshotRetentionLimit: 1

Outputs:
    RedisAddress:
        Description: Redis endpoint address
        Value: !GetAtt 'RedisService.RedisEndpoint.Address'

    RedisPort:
        Description: Redis endpoint port
        Value: !GetAtt 'RedisService.RedisEndpoint.Port'


================================================
FILE: cloudformation/infrastructure/elasticache.yaml
================================================
Description: >
    This template deploys an elasticache cluster to the provided VPC and subnets

Parameters:
    EnvironmentName:
        Description: An environment name that will be prefixed to resource names
        Type: String

    PrivateSubnets:
        Description: Choose which subnets this ECS cluster should be deployed to
        Type: List<AWS::EC2::Subnet::Id>

    SecurityGroup:
        Description: Select the Security Group to use for the ECS cluster hosts
        Type: AWS::EC2::SecurityGroup::Id

    CacheNodeType:
        Type: String
        Default: cache.m1.small

Resources:
    CachePrivateSubnetGroup:
        UpdateReplacePolicy: Retain
        Type: AWS::ElastiCache::SubnetGroup
        DeletionPolicy: Retain
        Properties:
            Description: Private subnet group
            SubnetIds: !Ref PrivateSubnets
    RedisService:
        UpdateReplacePolicy: Retain
        Type: AWS::ElastiCache::CacheCluster
        DeletionPolicy: Retain
        Properties:
            VpcSecurityGroupIds:
                - !Ref 'SecurityGroup'
            CacheSubnetGroupName: !Ref 'CachePrivateSubnetGroup'
            CacheNodeType: !Ref 'CacheNodeType'
            ClusterName: !Sub '${EnvironmentName}-redis'
            Engine: redis
            AutoMinorVersionUpgrade: true
            NumCacheNodes: 1
            SnapshotRetentionLimit: 1

Outputs:
    RedisAddress:
        Description: Redis endpoint address
        Value: !GetAtt 'RedisService.RedisEndpoint.Address'

    RedisPort:
        Description: Redis endpoint port
        Value: !GetAtt 'RedisService.RedisEndpoint.Port'


================================================
FILE: cloudformation/infrastructure/elasticsearch.yaml
================================================
Description: >
    This template deploys a VPC-based ElasticSearch cluster.

Parameters:
    EnvName:
        Type: String
        Description: which environment to target
        AllowedValues:
            - 'dev'
            - 'test'
            - 'stage'
            - 'prod'
        ConstraintDescription: Must match a location for secret storage in secretsmanager

    SecurityGroup:
        Description: Select the Security Group to use for the ECS cluster hosts
        Type: AWS::EC2::SecurityGroup::Id

    PrivateSubnet2:
        Description: The private subnet in AZ2 for the VPC
        Type: AWS::EC2::Subnet::Id

Resources:
    ESCluster:
        Type: AWS::Elasticsearch::Domain
        Properties:
            ElasticsearchClusterConfig:
                InstanceCount: 1
                ZoneAwarenessEnabled: false
                InstanceType: 'm5.xlarge.elasticsearch'
            ElasticsearchVersion: '7.10'
            EBSOptions:
                EBSEnabled: true
                Iops: 0
                VolumeSize: 20
                VolumeType: 'standard'
            SnapshotOptions:
                AutomatedSnapshotStartHour: 0
            AccessPolicies:
                Version: '2012-10-17'
                Statement:
                    - Effect: 'Allow'
                      Principal:
                          AWS: '*'
                      Action: 'es:*'
                      Resource: !Sub 'arn:aws:es:us-east-1:619333082511:domain/crowd-${EnvName}-vpc/*'
            AdvancedOptions:
                rest.action.multi.allow_explicit_index: 'true'
            Tags:
                - Key: Environment
                  Value: !Ref EnvName
            VPCOptions:
                SubnetIds:
                    - Ref: PrivateSubnet2
                SecurityGroupIds:
                    - Ref: SecurityGroup


================================================
FILE: cloudformation/infrastructure/fargate-cluster.yaml
================================================
Description: >
    This template deploys a fargate cluster to the provided VPC and subnets

Parameters:
    EnvironmentName:
        Description: An environment name that will be prefixed to resource names
        Type: String

    PublicSubnets:
        Description: The subnets for the load balancer
        Type: List<AWS::EC2::Subnet::Id>

    PrivateSubnets:
        Description: Choose which subnets this ECS cluster should be deployed to
        Type: List<AWS::EC2::Subnet::Id>

    SecurityGroup:
        Description: Select the Security Group to use for the ECS cluster hosts
        Type: AWS::EC2::SecurityGroup::Id

    LoadBalancerSecurityGroup:
        Description: The SecurityGroup for load balancer
        Type: AWS::EC2::SecurityGroup::Id

    VpcId:
        Description: The Id of the VPC for this cluster
        Type: AWS::EC2::VPC::Id

    ConcordiaVersion:
        Type: String
        Description: version of concordia docker images to pull and deploy
        Default: latest

    DjangoKeyId:
        Type: String
        Description: unique ID appended to end of DjangoSecretKey ARN in secrets manager
        Default: xxxxx

    DbSecretId:
        Type: String
        Description: unique ID appended to end of DB password ARN in secrets manager
        Default: xxxxx

    EnvName:
        Type: String
        Description: which environment to target
        AllowedValues:
            - 'dev'
            - 'test'
            - 'stage'
            - 'prod'
            - 'cftest2'
        ConstraintDescription: Must match a location for secret storage in secretsmanager

    FullEnvironmentName:
        Type: String
        Description: Full name of deployment environment
        AllowedValues:
            - 'development'
            - 'test'
            - 'staging'
            - 'production'

    RedisAddress:
        Type: String
        Description: Redis endpoint address

    RedisPort:
        Type: String
        Description: Redis endpoint port

    CanonicalHostName:
        Type: String
        Description: canonical host name of the application, e.g. crowd-test.loc.gov

    DatabaseEndpoint:
        Type: String
        Description: Host name of the Postgres RDS service

    S3BucketName:
        Type: String
        Description: name of the S3 bucket (public) where collection images will be stored

    ExportS3BucketName:
        Type: String
        Description: name of the S3 bucket (public) where exported transcriptions will be stored

Resources:
    ConcordiaS3BucketAccessPolicy:
        UpdateReplacePolicy: Retain
        Type: AWS::IAM::Policy
        Metadata:
            cfn_nag:
                rules_to_suppress:
                    - id: W12
                      reason: 'S3 buckets must be specified with /* after the bucket name'
        DeletionPolicy: Retain
        Properties:
            PolicyName: !Sub ConcordiaServiceS3BucketAccess-${EnvironmentName}
            Roles:
                - !Ref 'ConcordiaTaskRole'
                - !Ref 'ConcordiaEC2Role'
            PolicyDocument:
                Version: '2012-10-17'
                Statement:
                    - Effect: Allow
                      Action:
                          - 's3:PutObject'
                          - 's3:GetObject'
                          - 's3:AbortMultipartUpload'
                          - 's3:ListMultipartUploadParts'
                          - 's3:ListBucket'
                          - 's3:ListBucketMultipartUploads'
                      Resource:
                          - !Sub 'arn:aws:s3:::crowd-${EnvironmentName}-content/*'
                          - !Sub 'arn:aws:s3:::crowd-${EnvironmentName}-export/*'

    ConcordiaKMSAccessPolicy:
        UpdateReplacePolicy: Retain
        Type: AWS::IAM::Policy
        DeletionPolicy: Retain
        Properties:
            PolicyName: !Sub ConcordiaServiceKMSAccess-${EnvironmentName}
            Roles:
                - !Ref 'ConcordiaTaskRole'
                - !Ref 'ConcordiaEC2Role'
            PolicyDocument:
                Version: '2012-10-17'
                Statement:
                    - Effect: Allow
                      Action:
                          - 'kms:GetParametersForImport'
                          - 'kms:GetKeyRotationStatus'
                          - 'kms:GetKeyPolicy'
                          - 'kms:DescribeKey'
                          - 'kms:ListResourceTags'
                          - 'kms:Decrypt'
                          - 'kms:GenerateDataKey'
                      Resource:
                          - 'arn:aws:kms:us-east-1:619333082511:key/d300e73d-9170-4001-933a-37af0bcdb956'

    ConcordiaServiceSecretAccessPolicy:
        UpdateReplacePolicy: Retain
        Type: AWS::IAM::Policy
        DeletionPolicy: Retain
        Properties:
            PolicyName: !Sub ConcordiaServiceSecretAccess-${EnvironmentName}
            Roles:
                - !Ref 'ConcordiaTaskRole'
                - !Ref 'ConcordiaEC2Role'
            PolicyDocument:
                Version: '2012-10-17'
                Statement:
                    - Effect: Allow
                      Action:
                          - 'secretsmanager:GetResourcePolicy'
                          - 'secretsmanager:GetSecretValue'
                          - 'secretsmanager:DescribeSecret'
                          - 'secretsmanager:ListSecretVersionIds'
                      Resource:
                          - 'arn:aws:secretsmanager:us-east-1:619333082511:secret:concordia/SMTP-GVlolk'
                          - !Sub 'arn:aws:secretsmanager:us-east-1:619333082511:secret:crowd/${EnvName}/Django/SecretKey-${DjangoKeyId}'
                          - !Sub 'arn:aws:secretsmanager:us-east-1:619333082511:secret:crowd/${EnvName}/DB/MasterUserPassword-${DbSecretId}'

    ConcordiaEC2Role:
        UpdateReplacePolicy: Retain
        Type: AWS::IAM::Role
        DeletionPolicy: Retain
        Properties:
            Path: /
            AssumeRolePolicyDocument:
                Version: '2012-10-17'
                Statement:
                    - Effect: Allow
                      Principal:
                          Service: ec2.amazonaws.com
                      Action: sts:AssumeRole
            ManagedPolicyArns:
                - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
                - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

    ConcordiaInstanceProfile:
        UpdateReplacePolicy: Retain
        Type: AWS::IAM::InstanceProfile
        DeletionPolicy: Retain
        Properties:
            Path: /
            Roles:
                - !Ref 'ConcordiaEC2Role'

    ConcordiaTaskRole:
        UpdateReplacePolicy: Retain
        Type: AWS::IAM::Role
        DeletionPolicy: Retain
        Properties:
            AssumeRolePolicyDocument:
                Version: '2012-10-17'
                Statement:
                    - Effect: Allow
                      Principal:
                          Service: ecs-tasks.amazonaws.com
                      Action:
                          - sts:AssumeRole
            ManagedPolicyArns:
                - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
                - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

    ConcordiaAppLogsGroup:
        UpdateReplacePolicy: Retain
        Type: AWS::Logs::LogGroup
        DeletionPolicy: Retain
        Properties:
            LogGroupName: !Ref AWS::StackName
            RetentionInDays: 30

    ConcordiaExternalTargetGroup:
        UpdateReplacePolicy: Retain
        Type: AWS::ElasticLoadBalancingV2::TargetGroup
        DeletionPolicy: Retain
        Properties:
            HealthCheckIntervalSeconds: 30
            HealthCheckPath: /healthz
            HealthCheckProtocol: HTTP
            HealthCheckTimeoutSeconds: 5
            HealthyThresholdCount: 2
            UnhealthyThresholdCount: 10
            TargetType: ip
            Port: 80
            Protocol: HTTP
            VpcId: !Ref VpcId

    LoadBalancer:
        UpdateReplacePolicy: Retain
        Type: AWS::ElasticLoadBalancingV2::LoadBalancer
        DeletionPolicy: Retain
        Properties:
            Subnets: !Ref PublicSubnets
            SecurityGroups:
                - !Ref LoadBalancerSecurityGroup

    ExternalLoadBalancerListener:
        UpdateReplacePolicy: Retain
        DeletionPolicy: Retain
        Properties:
            DefaultActions:
                # FIXME: When AWS CF supports it, redirect to https
                # instead of forward to target group
                - TargetGroupArn: !Ref ConcordiaExternalTargetGroup
                  Type: forward
            LoadBalancerArn: !Ref LoadBalancer
            Port: 80
            Protocol: HTTP
        Type: AWS::ElasticLoadBalancingV2::Listener

    SecureExternalLoadBalancerListener:
        UpdateReplacePolicy: Retain
        DeletionPolicy: Retain
        Properties:
            Certificates:
                - CertificateArn: !Sub 'arn:aws:iam::${AWS::AccountId}:server-certificate/${CanonicalHostName}'
            DefaultActions:
                - TargetGroupArn: !Ref ConcordiaExternalTargetGroup
                  Type: forward
            LoadBalancerArn: !Ref LoadBalancer
            Port: 443
            Protocol: HTTPS
        Type: AWS::ElasticLoadBalancingV2::Listener

    ECSCluster:
        UpdateReplacePolicy: Retain
        Type: AWS::ECS::Cluster
        DeletionPolicy: Retain
        Properties:
            ClusterName: !Ref EnvironmentName

    ConcordiaTask:
        UpdateReplacePolicy: Retain
        Type: AWS::ECS::TaskDefinition
        DeletionPolicy: Retain
        Properties:
            Family: !Sub crowd-${EnvName}
            Cpu: '4096'
            Memory: '16384'
            NetworkMode: awsvpc
            RequiresCompatibilities:
                - FARGATE
            ExecutionRoleArn: !GetAtt ConcordiaTaskRole.Arn
            TaskRoleArn: !GetAtt ConcordiaTaskRole.Arn
            Volumes:
                - Name: images_volume
            ContainerDefinitions:
                - Name: app
                  Cpu: 2048
                  Image: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/concordia:${ConcordiaVersion}'
                  LogConfiguration:
                      LogDriver: awslogs
                      Options:
                          awslogs-group: !Ref 'ConcordiaAppLogsGroup'
                          awslogs-region: !Ref 'AWS::Region'
                          awslogs-stream-prefix: ConcordiaServer
                  Environment:
                      - Name: AWS
                        Value: '1'
                      - Name: ENV_NAME
                        Value: !Ref EnvName
                      - Name: CONCORDIA_ENVIRONMENT
                        Value: !Ref FullEnvironmentName
                      - Name: S3_BUCKET_NAME
                        Value: !Ref S3BucketName
                      - Name: EXPORT_S3_BUCKET_NAME
                        Value: !Ref ExportS3BucketName
                      - Name: CELERY_BROKER_URL
                        Value: !Sub 'redis://${RedisAddress}:${RedisPort}/0'
                      - Name: AWS_DEFAULT_REGION
                        Value: !Ref AWS::Region
                      - Name: SENTRY_BACKEND_DSN
                        Value: https://6727341eabcd47e3a48ce300432e840b@errorlogging.loc.gov/5
                      - Name: SENTRY_FRONTEND_DSN
                        Value: https://77a13a941ffd485dbf41dbf8e7a0bdd0@errorlogging.loc.gov/4
                      - Name: REDIS_ADDRESS
                        Value: !Ref RedisAddress
                      - Name: REDIS_PORT
                        Value: !Ref RedisPort
                      - Name: POSTGRESQL_HOST
                        Value: !Ref DatabaseEndpoint
                      - Name: HOST_NAME
                        Value: !Ref CanonicalHostName
                      - Name: DJANGO_SETTINGS_MODULE
                        Value: concordia.settings_ecs
                  MountPoints:
                      - SourceVolume: images_volume
                        ContainerPath: /concordia_images
                  PortMappings:
                      - ContainerPort: 80
                - Name: importer
                  Cpu: 1024
                  Image: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/concordia/importer:${ConcordiaVersion}'
                  LogConfiguration:
                      LogDriver: awslogs
                      Options:
                          awslogs-group: !Ref 'ConcordiaAppLogsGroup'
                          awslogs-region: !Ref 'AWS::Region'
                          awslogs-stream-prefix: ConcordiaWorker
                  Environment:
                      - Name: AWS
                        Value: '1'
                      - Name: ENV_NAME
                        Value: !Ref EnvName
                      - Name: CONCORDIA_ENVIRONMENT
                        Value: !Ref FullEnvironmentName
                      - Name: S3_BUCKET_NAME
                        Value: !Ref S3BucketName
                      - Name: EXPORT_S3_BUCKET_NAME
                        Value: !Ref ExportS3BucketName
                      - Name: CELERY_BROKER_URL
                        Value: !Sub 'redis://${RedisAddress}:${RedisPort}/0'
                      - Name: AWS_DEFAULT_REGION
                        Value: !Ref AWS::Region
                      - Name: SENTRY_BACKEND_DSN
                        Value: https://6727341eabcd47e3a48ce300432e840b@errorlogging.loc.gov/5
                      - Name: SENTRY_FRONTEND_DSN
                        Value: https://77a13a941ffd485dbf41dbf8e7a0bdd0@errorlogging.loc.gov/4
                      - Name: REDIS_ADDRESS
                        Value: !Ref RedisAddress
                      - Name: REDIS_PORT
                        Value: !Ref RedisPort
                      - Name: POSTGRESQL_HOST
                        Value: !Ref DatabaseEndpoint
                      - Name: HOST_NAME
                        Value: !Ref CanonicalHostName
                      - Name: DJANGO_SETTINGS_MODULE
                        Value: concordia.settings_ecs
                  MountPoints:
                      - SourceVolume: images_volume
                        ContainerPath: /concordia_images
                - Name: celerybeat
                  Cpu: 1024
                  Image: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/concordia/celerybeat:${ConcordiaVersion}'
                  LogConfiguration:
                      LogDriver: awslogs
                      Options:
                          awslogs-group: !Ref 'ConcordiaAppLogsGroup'
                          awslogs-region: !Ref 'AWS::Region'
                          awslogs-stream-prefix: ConcordiaWorker
                  Environment:
                      - Name: AWS
                        Value: '1'
                      - Name: ENV_NAME
                        Value: !Ref EnvName
                      - Name: CONCORDIA_ENVIRONMENT
                        Value: !Ref FullEnvironmentName
                      - Name: S3_BUCKET_NAME
                        Value: !Ref S3BucketName
                      - Name: EXPORT_S3_BUCKET_NAME
                        Value: !Ref ExportS3BucketName
                      - Name: CELERY_BROKER_URL
                        Value: !Sub 'redis://${RedisAddress}:${RedisPort}/0'
                      - Name: AWS_DEFAULT_REGION
                        Value: !Ref AWS::Region
                      - Name: SENTRY_BACKEND_DSN
                        Value: https://6727341eabcd47e3a48ce300432e840b@errorlogging.loc.gov/5
                      - Name: SENTRY_FRONTEND_DSN
                        Value: https://77a13a941ffd485dbf41dbf8e7a0bdd0@errorlogging.loc.gov/4
                      - Name: REDIS_ADDRESS
                        Value: !Ref RedisAddress
                      - Name: REDIS_PORT
                        Value: !Ref RedisPort
                      - Name: POSTGRESQL_HOST
                        Value: !Ref DatabaseEndpoint
                      - Name: HOST_NAME
                        Value: !Ref CanonicalHostName
                      - Name: DJANGO_SETTINGS_MODULE
                        Value: concordia.settings_ecs

    ConcordiaExternalService:
        UpdateReplacePolicy: Retain
        Type: AWS::ECS::Service
        DependsOn: ExternalLoadBalancerListener
        DeletionPolicy: Retain
        Properties:
            Cluster: !Ref ECSCluster
            LaunchType: FARGATE
            DeploymentConfiguration:
                MaximumPercent: 200
                MinimumHealthyPercent: 75
            DesiredCount: 1
            NetworkConfiguration:
                AwsvpcConfiguration:
                    SecurityGroups:
                        - !Ref SecurityGroup
                    Subnets: !Ref PrivateSubnets
            TaskDefinition: !Ref ConcordiaTask
            LoadBalancers:
                - ContainerName: 'app'
                  ContainerPort: 80
                  TargetGroupArn: !Ref ConcordiaExternalTargetGroup

Outputs:
    LoadBalancerUrl:
        Description: The URL of the ALB
        Value: !GetAtt LoadBalancer.DNSName


================================================
FILE: cloudformation/infrastructure/fargate-featurebranch.yaml
================================================
Description: >
    This template deploys a fargate cluster to the provided VPC and subnets

Parameters:
    SecurityGroup:
        Description: Select the Security Group to use for the ECS cluster hosts
        Type: AWS::EC2::SecurityGroup::Id

    VpcId:
        Description: The Id of the VPC for this cluster
        Type: AWS::EC2::VPC::Id

    ConcordiaVersion:
        Type: String
        Description: docker tag of concordia app image to pull and deploy
        Default: latest

    EnvName:
        Type: String
        Description: which environment to target
        AllowedValues:
            - 'dev'
            - 'test'
            - 'stage'
            - 'prod'
        ConstraintDescription: Must match a location for secret storage in secretsmanager

    FullEnvironmentName:
        Type: String
        Description: Full name of deployment environment
        AllowedValues:
            - 'development'
            - 'test'
            - 'staging'
            - 'production'

    RedisAddress:
        Type: String
        Description: Redis endpoint address

    RedisPort:
        Type: String
        Description: Redis endpoint port

    CanonicalHostName:
        Type: String
        Description: canonical host name of the application, e.g. crowd-test.loc.gov

    DatabaseEndpoint:
        Type: String
        Description: Host name of the Postgres RDS service

    S3BucketName:
        Type: String
        Description: name of the S3 bucket (public) where collection images will be stored

    ExportS3BucketName:
        Type: String
        Description: name of the S3 bucket (public) where exported transcriptions will be stored

    Priority:
        Type: Number
        Description: Priority of the subdomain listener rule, must be unique in the set of listener rules
        Default: 100

    DataLoadStackName:
        Type: String
        Description: Signal that the DataLoadHost UserData has completed

Resources:
    ConcordiaAppLogsGroup:
        Type: AWS::Logs::LogGroup
        Properties:
            LogGroupName: !Ref AWS::StackName
            RetentionInDays: 30

    ConcordiaExternalTargetGroup:
        Type: AWS::ElasticLoadBalancingV2::TargetGroup
        Properties:
            HealthCheckIntervalSeconds: 30
            HealthCheckPath: /healthz
            HealthCheckProtocol: HTTP
            HealthCheckTimeoutSeconds: 5
            HealthyThresholdCount: 2
            UnhealthyThresholdCount: 10
            TargetType: ip
            Port: 80
            Protocol: HTTP
            VpcId: !Ref VpcId

    SubdomainListenerRule:
        Type: AWS::ElasticLoadBalancingV2::ListenerRule
        Properties:
            Actions:
                - TargetGroupArn: !Ref ConcordiaExternalTargetGroup
                  Type: forward
            Conditions:
                - Field: host-header
                  Values:
                      - !Ref CanonicalHostName
            ListenerArn: arn:aws:elasticloadbalancing:us-east-1:619333082511:listener/app/crowd-test/81e4820e354ea810/187fd94e534ad833
            Priority: !Ref Priority

    ConcordiaTask:
        Type: AWS::ECS::TaskDefinition
        Properties:
            Family: !Sub crowd-${ConcordiaVersion}
            Cpu: '4096'
            Memory: '30720'
            NetworkMode: awsvpc
            RequiresCompatibilities:
                - FARGATE
            ExecutionRoleArn: !Sub 'arn:aws:iam::${AWS::AccountId}:role/ConcordiaServerTaskRole-crowd-test'
            TaskRoleArn: !Sub 'arn:aws:iam::${AWS::AccountId}:role/ConcordiaServerTaskRole-crowd-test'
            Volumes:
                - Name: images_volume
            ContainerDefinitions:
                - Name: app
                  Cpu: 2048
                  Image: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/concordia:${ConcordiaVersion}'
                  LogConfiguration:
                      LogDriver: awslogs
                      Options:
                          awslogs-group: !Ref 'ConcordiaAppLogsGroup'
                          awslogs-region: !Ref 'AWS::Region'
                          awslogs-stream-prefix: ConcordiaServer
                  Environment:
                      - Name: AWS
                        Value: '1'
                      - Name: ENV_NAME
                        Value: !Ref EnvName
                      - Name: CONCORDIA_ENVIRONMENT
                        Value: !Ref FullEnvironmentName
                      - Name: S3_BUCKET_NAME
                        Value: !Ref S3BucketName
                      - Name: EXPORT_S3_BUCKET_NAME
                        Value: !Ref ExportS3BucketName
                      - Name: CELERY_BROKER_URL
                        Value: !Sub 'redis://${RedisAddress}:${RedisPort}/0'
                      - Name: AWS_DEFAULT_REGION
                        Value: !Ref AWS::Region
                      - Name: SENTRY_BACKEND_DSN
                        Value: https://6727341eabcd47e3a48ce300432e840b@errorlogging.loc.gov/5
                      - Name: SENTRY_FRONTEND_DSN
                        Value: https://77a13a941ffd485dbf41dbf8e7a0bdd0@errorlogging.loc.gov/4
                      - Name: REDIS_ADDRESS
                        Value: !Ref RedisAddress
                      - Name: REDIS_PORT
                        Value: !Ref RedisPort
                      - Name: POSTGRESQL_HOST
                        Value: !Ref DatabaseEndpoint
                      - Name: HOST_NAME
                        Value: !Ref CanonicalHostName
                      - Name: DJANGO_SETTINGS_MODULE
                        Value: concordia.settings_ecs
                  MountPoints:
                      - SourceVolume: images_volume
                        ContainerPath: /concordia_images
                  PortMappings:
                      - ContainerPort: 80
                - Name: importer
                  Cpu: 1024
                  Image: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/concordia/importer:${ConcordiaVersion}'
                  LogConfiguration:
                      LogDriver: awslogs
                      Options:
                          awslogs-group: !Ref 'ConcordiaAppLogsGroup'
                          awslogs-region: !Ref 'AWS::Region'
                          awslogs-stream-prefix: ConcordiaWorker
                  Environment:
                      - Name: AWS
                        Value: '1'
                      - Name: ENV_NAME
                        Value: !Ref EnvName
                      - Name: CONCORDIA_ENVIRONMENT
                        Value: !Ref FullEnvironmentName
                      - Name: S3_BUCKET_NAME
                        Value: !Ref S3BucketName
                      - Name: EXPORT_S3_BUCKET_NAME
                        Value: !Ref ExportS3BucketName
                      - Name: CELERY_BROKER_URL
                        Value: !Sub 'redis://${RedisAddress}:${RedisPort}/0'
                      - Name: AWS_DEFAULT_REGION
                        Value: !Ref AWS::Region
                      - Name: SENTRY_BACKEND_DSN
                        Value: https://6727341eabcd47e3a48ce300432e840b@errorlogging.loc.gov/5
                      - Name: SENTRY_FRONTEND_DSN
                        Value: https://77a13a941ffd485dbf41dbf8e7a0bdd0@errorlogging.loc.gov/4
                      - Name: REDIS_ADDRESS
                        Value: !Ref RedisAddress
                      - Name: REDIS_PORT
                        Value: !Ref RedisPort
                      - Name: POSTGRESQL_HOST
                        Value: !Ref DatabaseEndpoint
                      - Name: HOST_NAME
                        Value: !Ref CanonicalHostName
                      - Name: DJANGO_SETTINGS_MODULE
                        Value: concordia.settings_ecs
                  MountPoints:
                      - SourceVolume: images_volume
                        ContainerPath: /concordia_images
                - Name: celerybeat
                  Cpu: 1024
                  Image: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/concordia/celerybeat:${ConcordiaVersion}'
                  LogConfiguration:
                      LogDriver: awslogs
                      Options:
                          awslogs-group: !Ref 'ConcordiaAppLogsGroup'
                          awslogs-region: !Ref 'AWS::Region'
                          awslogs-stream-prefix: ConcordiaWorker
                  Environment:
                      - Name: AWS
                        Value: '1'
                      - Name: ENV_NAME
                        Value: !Ref EnvName
                      - Name: CONCORDIA_ENVIRONMENT
                        Value: !Ref FullEnvironmentName
                      - Name: S3_BUCKET_NAME
                        Value: !Ref S3BucketName
                      - Name: EXPORT_S3_BUCKET_NAME
                        Value: !Ref ExportS3BucketName
                      - Name: CELERY_BROKER_URL
                        Value: !Sub 'redis://${RedisAddress}:${RedisPort}/0'
                      - Name: AWS_DEFAULT_REGION
                        Value: !Ref AWS::Region
                      - Name: SENTRY_BACKEND_DSN
                        Value: https://6727341eabcd47e3a48ce300432e840b@errorlogging.loc.gov/5
                      - Name: SENTRY_FRONTEND_DSN
                        Value: https://77a13a941ffd485dbf41dbf8e7a0bdd0@errorlogging.loc.gov/4
                      - Name: REDIS_ADDRESS
                        Value: !Ref RedisAddress
                      - Name: REDIS_PORT
                        Value: !Ref RedisPort
                      - Name: POSTGRESQL_HOST
                        Value: !Ref DatabaseEndpoint
                      - Name: HOST_NAME
                        Value: !Ref CanonicalHostName
                      - Name: DJANGO_SETTINGS_MODULE
                        Value: concordia.settings_ecs

    ConcordiaExternalService:
        Type: AWS::ECS::Service
        Properties:
            Cluster: crowd-test
            LaunchType: FARGATE
            DeploymentConfiguration:
                DeploymentCircuitBreaker:
                    Enable: true
                    Rollback: false
                MaximumPercent: 200
                MinimumHealthyPercent: 75
            DesiredCount: 1
            EnableExecuteCommand: true
            NetworkConfiguration:
                AwsvpcConfiguration:
                    SecurityGroups:
                        - !Ref SecurityGroup
                    Subnets:
                        - subnet-0aa55b322229b945a
                        - subnet-0f65558b319b2d4dc
            TaskDefinition: !Ref ConcordiaTask
            LoadBalancers:
                - ContainerName: 'app'
                  ContainerPort: 80
                  TargetGroupArn: !Ref ConcordiaExternalTargetGroup


================================================
FILE: cloudformation/infrastructure/jenkins-server.yaml
================================================
Description: This template deploys an Ubuntu jenkins server in the default VPC.

Resources:
    Jenkins:
        Type: AWS::EC2::Instance
        Properties:
            ImageId: 'ami-042e8287309f5df03'
            InstanceType: 't2.xlarge'
            IamInstanceProfile: 'concordia-jenkins-ec2-role'
            BlockDeviceMappings:
                - DeviceName: /dev/sda1
                  Ebs:
                      VolumeSize: 128
                      VolumeType: gp3
                      DeleteOnTermination: true
            NetworkInterfaces:
                - AssociatePublicIpAddress: true
                  DeviceIndex: '0'
                  GroupSet:
                      - 'sg-02ff28781d04fd191'
                  SubnetId: 'subnet-3748107d'
            UserData:
                Fn::Base64: !Sub |
                    #!/bin/bash -xe
                    echo "Running userdata for ${AWS::StackName}"
                    wget -q -O - https://pkg.jenkins.io/debian/jenkins.io.key | apt-key add -
                    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
                    sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list'
                    add-apt-repository \
                      "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
                      $(lsb_release -cs) \
                      stable"
                    apt-get update
                    apt-get install -qy -o Dpkg::Options::='--force-confnew' \
                      python3 python3-dev python3-venv python3-pip \
                      libtiff-dev libmemcached-dev libjpeg-dev libopenjp2-7-dev libwebp-dev zlib1g-dev \
                      graphviz apt-transport-https libpq-dev \
                      ca-certificates \
                      curl \
                      gnupg-agent \
                      software-properties-common \
                      docker-ce docker-ce-cli containerd.io \
                      openjdk-8-jdk jenkins \
                      nginx awscli
                    usermod -aG docker jenkins
                    snap install postgresql12
                    pip3 install awscli --upgrade
            Tags:
                - Key: Name
                  Value: Jenkins
                - Key: Environment
                  Value: dev


================================================
FILE: cloudformation/infrastructure/network-acl.yaml
================================================
Description: >
    This template contains the security groups required by our entire stack.
    We create them in a seperate nested template, so they can be referenced
    by all of the other nested templates.

Parameters:
    EnvironmentName:
        Description: An environment name that will be prefixed to resource names
        Type: String

    VPC:
        Type: AWS::EC2::VPC::Id
        Description: Choose which VPC the security groups should be deployed to

    PublicSubnet1:
        Description: A reference to the public subnet in the 1st Availability Zone
        Type: AWS::EC2::Subnet::Id

    PublicSubnet2:
        Description: A reference to the public subnet in the 2nd Availability Zone
        Type: AWS::EC2::Subnet::Id

    PrivateSubnet1:
        Description: A reference to the private subnet in the 1st Availability Zone
        Type: AWS::EC2::Subnet::Id

    PrivateSubnet2:
        Description: A reference to the private subnet in the 2nd Availability Zone
        Type: AWS::EC2::Subnet::Id

Resources:
    NetworkAcl:
        UpdateReplacePolicy: Retain
        Type: AWS::EC2::NetworkAcl
        DeletionPolicy: Retain
        Properties:
            VpcId:
                Ref: VPC
            Tags:
                - Key: Name
                  Value: !Ref EnvironmentName

    # TODO: Update these ACLs to the latest OCIO standard ones
    # NOTE: These rules are for dev / test / stage only

    acl4:
        UpdateReplacePolicy: Retain
        Type: AWS::EC2::NetworkAclEntry
        DeletionPolicy: Retain
        Properties:
            CidrBlock: 0.0.0.0/0
            Egress: true
            Protocol: -1
            RuleAction: allow
            RuleNumber: 100
            NetworkAclId: !Ref NetworkAcl
    acl5:
        UpdateReplacePolicy: Retain
        Type: AWS::EC2::NetworkAclEntry
        DeletionPolicy: Retain
        Properties:
            CidrBlock: 140.147.236.152/32
            Protocol: -1
            RuleAction: deny
            RuleNumber: 10
            NetworkAclId: !Ref NetworkAcl
    acl6:
        UpdateReplacePolicy: Retain
        Type: AWS::EC2::NetworkAclEntry
        DeletionPolicy: Retain
        Properties:
            CidrBlock: 140.147.236.214/32
            Protocol: -1
            RuleAction: deny
            RuleNumber: 11
            NetworkAclId: !Ref NetworkAcl
    acl6b:
        UpdateReplacePolicy: Retain
        Type: AWS::EC2::NetworkAclEntry
        DeletionPolicy: Retain
        Properties:
            CidrBlock: 140.147.236.213/32
            Protocol: -1
            RuleAction: deny
            RuleNumber: 12
            NetworkAclId: !Ref NetworkAcl
    acl7:
        UpdateReplacePolicy: Retain
        Type: AWS::EC2::NetworkAclEntry
        DeletionPolicy: Retain
        Properties:
            CidrBlock: 140.147.0.0/16
            Protocol: 6
            RuleAction: allow
            RuleNumber: 100
            PortRange:
                From: 22
                To: 22
            NetworkAclId:
                Ref: NetworkAcl
    acl8:
        UpdateReplacePolicy: Retain
        Type: AWS::EC2::NetworkAclEntry
        DeletionPolicy: Retain
        Properties:
            CidrBlock: 0.0.0.0/0
            Protocol: 6
            RuleAction: allow
            RuleNumber: 110
            PortRange:
                From: 1024
                To: 65535
            NetworkAclId:
                Ref: NetworkAcl
    acl9:
        UpdateReplacePolicy: Retain
        Type: AWS::EC2::NetworkAclEntry
        DeletionPolicy: Retain
        Properties:
            CidrBlock: 0.0.0.0/0
            Protocol: 6
            RuleAction: allow
            RuleNumber: 200
            PortRange:
                From: 80
                To: 80
            NetworkAclId: !Ref NetworkAcl
    acl10:
        UpdateReplacePolicy: Retain
        Type: AWS::EC2::NetworkAclEntry
        DeletionPolicy: Retain
        Properties:
            CidrBlock: 0.0.0.0/0
            Protocol: 6
            RuleAction: allow
            RuleNumber: 210
            PortRange:
                From: 443
                To: 443
            NetworkAclId: !Ref NetworkAcl

    acl11:
        UpdateReplacePolicy: Retain
        Type: AWS::EC2::NetworkAclEntry
        DeletionPolicy: Retain
        Properties:
            CidrBlock: 0.0.0.0/0
            Protocol: -1
            RuleAction: allow
            RuleNumber: 300
            NetworkAclId: !Ref NetworkAcl

    subnetacl5:
        UpdateReplacePolicy: Retain
        Type: AWS::EC2::SubnetNetworkAclAssociation
        DeletionPolicy: Retain
        Properties:
            NetworkAclId: !Ref NetworkAcl
            SubnetId: !Ref PrivateSubnet1

    subnetacl6:
        UpdateReplacePolicy: Retain
        Type: AWS::EC2::SubnetNetworkAclAssociation
        DeletionPolicy: Retain
        Properties:
            NetworkAclId: !Ref NetworkAcl
            SubnetId: !Ref PrivateSubnet2

    subnetacl7:
        UpdateReplacePolicy: Retain
        Type: AWS::EC2::SubnetNetworkAclAssociation
        DeletionPolicy: Retain
        Properties:
            NetworkAclId: !Ref NetworkAcl
            SubnetId: !Ref PublicSubnet1

    subnetacl8:
        UpdateReplacePolicy: Retain
        Type: AWS::EC2::SubnetNetworkAclAssociation
        DeletionPolicy: Retain
        Properties:
            NetworkAclId: !Ref NetworkAcl
            SubnetId: !Ref PublicSubnet2


================================================
FILE: cloudformation/infrastructure/opensearch.yaml
================================================
Description: >
    This template deploys a VPC-based OpenSearch cluster.

Parameters:
    EnvName:
        Type: String
        Description: which environment to target
        AllowedValues:
            - 'dev'
            - 'test'
            - 'stage'
            - 'prod'
        ConstraintDescription: Must match a location for secret storage in secretsmanager

    SecurityGroup:
        Description: Select the Security Group to use for the ECS cluster hosts
        Type: AWS::EC2::SecurityGroup::Id

    PrivateSubnet2:
        Description: The private subnet in AZ2 for the VPC
        Type: AWS::EC2::Subnet::Id

Resources:
    ESCluster:
        Type: AWS::OpenSearchService::Domain
        Properties:
            ClusterConfig:
                InstanceCount: 1
                ZoneAwarenessEnabled: false
                InstanceType: 'm7g.xlarge.search'
            EngineVersion: '1.3'
            EBSOptions:
                EBSEnabled: true
                Iops: 0
                VolumeSize: 30
                VolumeType: 'gp3'
            SnapshotOptions:
                AutomatedSnapshotStartHour: 0
            AccessPolicies:
                Version: '2012-10-17'
                Statement:
                    - Effect: 'Allow'
                      Principal:
                          AWS: '*'
                      Action: 'es:*'
                      Resource: !Sub 'arn:aws:es:us-east-1:619333082511:domain/crowd-${EnvName}-vpc/*'
            AdvancedOptions:
                rest.action.multi.allow_explicit_index: 'true'
            Tags:
                - Key: Environment
                  Value: !Ref EnvName
            VPCOptions:
                SubnetIds:
                    - Ref: PrivateSubnet2
                SecurityGroupIds:
                    - Ref: SecurityGroup


================================================
FILE: cloudformation/infrastructure/rds.yaml
================================================
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
    DatabaseSecurityGroup:
        Description: Sets the security group to use for RDS database access
        Type: AWS::EC2::SecurityGroup::Id

    PrivateSubnet1:
        Description: A reference to the private subnet in the 1st Availability Zone
        Type: AWS::EC2::Subnet::Id

    PrivateSubnet2:
        Description: A reference to the private subnet in the 2nd Availability Zone
        Type: AWS::EC2::Subnet::Id

    DbUsername:
        Description: The username to use for the database
        Type: String
        NoEcho: true

    DbPassword:
        Description: The password to use for the database
        Type: String
        NoEcho: true

Resources:
    PostgresSubnetGroup:
        UpdateReplacePolicy: Retain
        Type: AWS::RDS::DBSubnetGroup
        DeletionPolicy: Retain
        Properties:
            DBSubnetGroupDescription: Created from the RDS Management Console
            SubnetIds:
                - Ref: PrivateSubnet1
                - Ref: PrivateSubnet2

    PostgresService:
        UpdateReplacePolicy: Retain
        Type: AWS::RDS::DBInstance
        DeletionPolicy: Retain
        Properties:
            AllocatedStorage: '20'
            AllowMajorVersionUpgrade: false
            AutoMinorVersionUpgrade: true
            DBInstanceClass: db.t4g.medium
            Port: '5432'
            PubliclyAccessible: false
            StorageType: gp3
            StorageEncrypted: True
            BackupRetentionPeriod: 31
            MasterUsername: !Ref DbUsername
            MasterUserPassword: !Ref DbPassword
            PreferredBackupWindow: 03:47-04:17
            PreferredMaintenanceWindow: tue:03:14-tue:03:44
            DBName: concordia
            Engine: postgres
            EngineVersion: '15.5'
            LicenseModel: postgresql-license
            DBSubnetGroupName:
                Ref: PostgresSubnetGroup
            VPCSecurityGroups:
                - Ref: DatabaseSecurityGroup
            Tags:
                - Key: workload-type
                  Value: other

Outputs:
    DatabaseHostName:
        Description: 'Hostname for the relational database service'
        Value: !GetAtt PostgresService.Endpoint.Address


================================================
FILE: cloudformation/infrastructure/search-proxy-task.yaml
================================================
Description: >
    This template deploys an opensearch dashboard proxy server to the specified VPC

Parameters:
    VpcId:
        Description: The Id of the VPC for this cluster
        Type: AWS::EC2::VPC::Id

    EnvName:
        Type: String
        Description: which environment to target
        AllowedValues:
            - 'dev'
            - 'test'
            - 'stage'
            - 'prod'
        ConstraintDescription: Must match a location for secret storage in secretsmanager

    Priority:
        Type: Number
        Description: Priority of the subdomain listener rule, must be unique in the set of listener rules
        Default: 100

Mappings:
    EnvironmentMapping:
        ListenerArn:
            dev: 'arn:aws:elasticloadbalancing:us-east-1:619333082511:listener/app/crowd-dev/112d22a79e25de0b/8bb4cb9c8b054e91'
            test: 'arn:aws:elasticloadbalancing:us-east-1:619333082511:listener/app/crowd-test/81e4820e354ea810/187fd94e534ad833'
            stage: 'arn:aws:elasticloadbalancing:us-east-1:619333082511:listener/app/crowd-stage/7d954bca84b62358/ab34414a68f355f2'
            prod: 'arn:aws:elasticloadbalancing:us-east-1:619333082511:listener/app/crowd-prod/746d0ae14ecc23e4/747212dd4e5706be'

        TaskRoleArn:
            dev: 'arn:aws:iam::619333082511:role/ConcordiaServerTaskRole-crowd-dev'
            test: 'arn:aws:iam::619333082511:role/ConcordiaServerTaskRole-crowd-test'
            stage: 'arn:aws:iam::619333082511:role/ConcordiaServerTaskRole-crowd-stage'
            prod: 'arn:aws:iam::619333082511:role/ConcordiaServerTaskRole-crowd-prod'

        # The ID of a private subnet
        # Type: AWS::EC2::Subnet::Id
        PrivateSubnet1:
            dev: subnet-0c95a830ce007fa65
            test: subnet-0aa55b322229b945a
            stage: subnet-0f7c7d66b66d6dd90
            prod: subnet-0da84976b66c32ce4

        OpensearchEndpoint:
            dev: 'https://vpc-crowd-dev-vpc-6xqqrxn5naqkvtdl6r6uanlhbe.us-east-1.es.amazonaws.com'
            test: 'https://vpc-crowd-test-vpc-63g3ylzduyzywhqbsqotnnm7ke.us-east-1.es.amazonaws.com'
            stage: 'https://vpc-crowd-stage-vpc-x5lgoj5yo76dvrxpfhmusss2b4.us-east-1.es.amazonaws.com'
            prod: 'https://vpc-crowd-prod-vpc-zl5xdhmtpr7squr6mtl7znqyqa.us-east-1.es.amazonaws.com'

        # The security group
        # Type: AWS::EC2::SecurityGroup::Id
        SecurityGroup:
            dev: sg-0ceb6b1dc0de899b3
            test: sg-09bc01194e6c52cb9
            stage: sg-0f6145067777b1cc3
            prod: sg-031594e2cfc8b25c7

Resources:
    DashboardLogsGroup:
        Type: AWS::Logs::LogGroup
        Properties:
            LogGroupName: !Ref AWS::StackName
            RetentionInDays: 30

    DashboardTargetGroup:
        Type: AWS::ElasticLoadBalancingV2::TargetGroup
        Properties:
            HealthCheckIntervalSeconds: 30
            HealthCheckPath: /
            HealthCheckProtocol: HTTP
            HealthCheckTimeoutSeconds: 5
            HealthyThresholdCount: 2
            UnhealthyThresholdCount: 10
            TargetType: ip
            Port: 80
            Protocol: HTTP
            VpcId: !Ref VpcId
            Matcher:
                HttpCode: '200,301' # Add this line for success codes
            Tags:
                - Key: Project
                  Value: Concordia
                - Key: Department
                  Value: OCIO
                - Key: ArcherID
                  Value: LIB-361
                - Key: Environment
                  Value: Development
                - Key: StackManaged
                  Value: crowd-dev-searchproxy

    SubdomainListenerRule:
        Type: AWS::ElasticLoadBalancingV2::ListenerRule
        Properties:
            Actions:
                - TargetGroupArn: !Ref DashboardTargetGroup
                  Type: forward
            Conditions:
                - Field: path-pattern
                  Values:
                      - '/_dashboards*'
            ListenerArn:
                Fn::FindInMap:
                    - EnvironmentMapping
                    - ListenerArn
                    - Ref: EnvName
            Priority: !Ref Priority

    DashboardTask:
        Type: AWS::ECS::TaskDefinition
        Properties:
            Family: !Sub crowd-${EnvName}-searchproxy
            Cpu: '256'
            Memory: '512'
            NetworkMode: awsvpc
            RequiresCompatibilities:
                - FARGATE
            ExecutionRoleArn: !Sub 'arn:aws:iam::${AWS::AccountId}:role/ecsTaskExecutionRole'
            TaskRoleArn:
                Fn::FindInMap:
                    - EnvironmentMapping
                    - TaskRoleArn
                    - Ref: EnvName
            ContainerDefinitions:
                - Name: sigv4proxy
                  Image: public.ecr.aws/aws-observability/aws-sigv4-proxy:1.10
                  Cpu: 256
                  Memory: 512
                  Essential: true
                  PortMappings:
                      - ContainerPort: 80
                        Protocol: tcp
                  LogConfiguration:
                      LogDriver: awslogs
                      Options:
                          awslogs-group: !Ref 'DashboardLogsGroup'
                          awslogs-region: !Ref 'AWS::Region'
                          awslogs-stream-prefix: ConcordiaDashboardProxy
                  Environment:
                      - Name: OPENSEARCH_ENDPOINT
                        Value:
                            Fn::FindInMap:
                                - EnvironmentMapping
                                - OpensearchEndpoint
                                - Ref: EnvName
                  Command:
                      - --name
                      - es
                      - --region
                      - !Ref AWS::Region
                      - --host
                      - !Select
                        - 1
                        - !Split
                          - '://'
                          - !FindInMap [
                                EnvironmentMapping,
                                OpensearchEndpoint,
                                !Ref EnvName,
                            ]
                      - --port
                      - '0.0.0.0:80'
                      - --sign-host
                      - !Select
                        - 1
                        - !Split
                          - '://'
                          - !FindInMap [
                                EnvironmentMapping,
                                OpensearchEndpoint,
                                !Ref EnvName,
                            ]
                      - --no-verify-ssl
            Tags:
                - Key: Project
                  Value: Concordia
                - Key: Department
                  Value: OCIO
                - Key: ArcherID
                  Value: LIB-361
                - Key: Environment
                  Value: Development
                - Key: StackManaged
                  Value: crowd-dev-searchproxy

    DashboardService:
        Type: AWS::ECS::Service
        Properties:
            Cluster: !Sub crowd-${EnvName}
            LaunchType: FARGATE
            DeploymentConfiguration:
                MaximumPercent: 200
                MinimumHealthyPercent: 100
            DesiredCount: 1
            NetworkConfiguration:
                AwsvpcConfiguration:
                    SecurityGroups:
                        - Fn::FindInMap:
                              - EnvironmentMapping
                              - SecurityGroup
                              - Ref: EnvName
                    Subnets:
                        - Fn::FindInMap:
                              - EnvironmentMapping
                              - PrivateSubnet1
                              - Ref: EnvName
            TaskDefinition: !Ref DashboardTask
            LoadBalancers:
                - ContainerName: 'sigv4proxy'
                  ContainerPort: 80
                  TargetGroupArn: !Ref DashboardTargetGroup
            EnableExecuteCommand: true
            Tags:
                - Key: Project
                  Value: Concordia
                - Key: Department
                  Value: OCIO
                - Key: ArcherID
                  Value: LIB-361
                - Key: Environment
                  Value: Development
                - Key: StackManaged
                  Value: crowd-dev-searchproxy


================================================
FILE: cloudformation/infrastructure/security-groups.yaml
================================================
Description: >
    This template contains the security groups required by our entire stack.
    We create them in a seperate nested template, so they can be referenced
    by all of the other nested templates.

Parameters:
    EnvironmentName:
        Description: An environment name that will be prefixed to resource names
        Type: String

    VPC:
        Type: AWS::EC2::VPC::Id
        Description: Choose which VPC the security groups should be deployed to

Resources:
    # This security group defines who/where is allowed to access the ECS hosts directly.
    # By default we're just allowing access from the load balancer.  If you want to SSH
    # into the hosts, or expose non-load balanced services you can open their ports here.
    ECSHostSecurityGroup:
        UpdateReplacePolicy: Retain
        Type: AWS::EC2::SecurityGroup
        DeletionPolicy: Retain
        Properties:
            VpcId: !Ref VPC
            GroupDescription: Access to the ECS hosts and the tasks/containers that run on them
            SecurityGroupIngress:
                - Description: 'Open access to container hosts from the load balancer'
                  SourceSecurityGroupId: !Ref LoadBalancerSecurityGroup
                  IpProtocol: '-1'
                - Description: 'SSH access to container hosts from bastion hosts'
                  SourceSecurityGroupId: !Ref BastionHostSecurityGroup
                  IpProtocol: tcp
                  FromPort: 22
                  ToPort: 22
            SecurityGroupEgress:
                - Description: 'Explicit outbound access'
                  IpProtocol: '-1'
                  CidrIp: 0.0.0.0/0

            Tags:
                - Key: Name
                  Value: !Sub ${EnvironmentName}-ECS-Hosts

    LoadBalancerSecurityGroup:
        UpdateReplacePolicy: Retain
        Type: AWS::EC2::SecurityGroup
        Metadata:
            cfn_nag:
                rules_to_suppress:
                    - id: W9
                      reason: 'The CIDR block should only allow 140.147.*.* IPs so it should end in /16'
        DeletionPolicy: Retain
        Properties:
            VpcId: !Ref VPC
            GroupDescription: Access to the load balancer that sits in front of ECS
            SecurityGroupIngress:
                - Description: 'Allow HTTP access from the LC network to our ECS services'
                  CidrIp: 140.147.0.0/16
                  IpProtocol: tcp
                  FromPort: 80
                  ToPort: 80
                - Description: 'Allow HTTPS access from the LC network to our ECS services'
                  CidrIp: 140.147.0.0/16
                  IpProtocol: tcp
                  FromPort: 443
                  ToPort: 443
            SecurityGroupEgress:
                - Description: 'Explicit outbound access'
                  IpProtocol: '-1'
                  CidrIp: 0.0.0.0/0
            Tags:
                - Key: Name
                  Value: !Sub ${EnvironmentName}-LoadBalancers
                - Key: AllowCloudFlareIngress
                  Value: 'true'

    DatabaseSecurityGroup:
        UpdateReplacePolicy: Retain
        Type: AWS::EC2::SecurityGroup
        DeletionPolicy: Retain
        Properties:
            VpcId: !Ref VPC
            GroupDescription: Access to the RDS Postgres database
            SecurityGroupIngress:
                - Description: 'Postgresql access to RDS from container hosts'
                  SourceSecurityGroupId: !Ref ECSHostSecurityGroup
                  IpProtocol: tcp
                  FromPort: 5432
                  ToPort: 5432
            SecurityGroupEgress:
                - Description: 'Explicit outbound access'
                  IpProtocol: '-1'
                  CidrIp: 0.0.0.0/0

    BastionHostSecurityGroup:
        UpdateReplacePolicy: Retain
        Type: AWS::EC2::SecurityGroup
        Metadata:
            cfn_nag:
                rules_to_suppress:
                    - id: W9
                      reason: 'The CIDR block should only allow 140.147.*.* IPs so it should end in /16'
        DeletionPolicy: Retain
        Properties:
            VpcId: !Ref VPC
            GroupDescription: Bastion hosts for ECS access
            SecurityGroupIngress:
                - Description: 'SSH access from LC network to bastion hosts'
                  CidrIp: 140.147.0.0/16
                  IpProtocol: tcp
                  FromPort: 22
                  ToPort: 22
            SecurityGroupEgress:
                - Description: 'Explicit outbound access'
                  IpProtocol: '-1'
                  CidrIp: 0.0.0.0/0
            Tags:
                - Key: Name
                  Value: !Sub ${EnvironmentName}-BastionHosts

    CacheServiceSecurityGroup:
        UpdateReplacePolicy: Retain
        Type: AWS::EC2::SecurityGroup
        DeletionPolicy: Retain
        Properties:
            VpcId: !Ref VPC
            GroupDescription: Access to cache services for ECS hosts
            SecurityGroupIngress:
                - Description: 'Redis service access from container hosts'
                  SourceSecurityGroupId: !Ref 'ECSHostSecurityGroup'
                  IpProtocol: tcp
                  FromPort: 6379
                  ToPort: 6379
            SecurityGroupEgress:
                - Description: 'Explicit outbound access'
                  IpProtocol: '-1'
                  CidrIp: 0.0.0.0/0

Outputs:
    ECSHostSecurityGroup:
        Description: A reference to the security group for ECS hosts
        Value: !Ref ECSHostSecurityGroup

    LoadBalancerSecurityGroup:
        Description: A reference to the security group for load balancers
        Value: !Ref LoadBalancerSecurityGroup

    DatabaseSecurityGroup:
        Description: A reference to the security group for RDS
        Value: !Ref DatabaseSecurityGroup

    BastionHostSecurityGroup:
        Description: A reference to the security group for bastion hosts
        Value: !Ref BastionHostSecurityGroup

    CacheServiceSecurityGroup:
        Description: A reference to the security group for cache services
        Value: !Ref CacheServiceSecurityGroup


================================================
FILE: cloudformation/infrastructure/vpc.yaml
================================================
Description: >
    This template deploys a VPC, with a pair of public and private subnets spread
    across two Availabilty Zones. It deploys an Internet Gateway, with a default
    route on the public subnets. It deploys a pair of NAT Gateways (one in each AZ),
    and default routes for them in the private subnets.

Parameters:
    EnvironmentName:
        Description: An environment name that will be prefixed to resource names
        Type: String

    VpcCIDR:
        Description: Please enter the IP range (CIDR notation) for this VPC
        Type: String
        Default: 10.192.0.0/16
        AllowedPattern: '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$'

    PublicSubnet1CIDR:
        Description: Please enter the IP range (CIDR notation) for the public subnet in the first Availability Zone
        Type: String
        Default: 10.192.10.0/24
        AllowedPattern: '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$'

    PublicSubnet2CIDR:
        Description: Please enter the IP range (CIDR notation) for the public subnet in the second Availability Zone
        Type: String
        Default: 10.192.11.0/24
        AllowedPattern: '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$'

    PrivateSubnet1CIDR:
        Description: Please enter the IP range (CIDR notation) for the private subnet in the first Availability Zone
        Type: String
        Default: 10.192.20.0/24
        AllowedPattern: '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$'

    PrivateSubnet2CIDR:
        Description: Please enter the IP range (CIDR notation) for the private subnet in the second Availability Zone
        Type: String
        Default: 10.192.21.0/24
        AllowedPattern: '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$'

    AvailabilityZone1:
        Description: The index of the availability zone for private and public subnet 1
        Type: Number
        Default: 0

    AvailabilityZone2:
        Description: The index of availability zone for private and public subnet 2
        Type: Number
        Default: 1

Resources:
    VPC:
        UpdateReplacePolicy: Retain
        Type: AWS::EC2::VPC
        DeletionPolicy: Retain
        Properties:
            CidrBlock: !Ref VpcCIDR
            InstanceTenancy: default
            EnableDnsHostnames: true
            EnableDnsSupport: true
            Tags:
                - Key: Name
                  Value: !Ref EnvironmentName

    InternetGateway:
        UpdateReplacePolicy: Retain
        Type: AWS::EC2::InternetGateway
        DeletionPolicy: Retain
        Properties:
            Tags:
                - Key: Name
                  Value: !Ref EnvironmentName

    InternetGatewayAttachment:
        UpdateReplacePolicy: Retain
        Type: AWS::EC2::VPCGatewayAttachment
        DeletionPolicy: Retain
        Properties:
            InternetGatewayId: !
Download .txt
gitextract_frv4ewgf/

├── .cfnlintrc.yaml
├── .dockerignore
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── black.yml
│       ├── build.yml
│       ├── codeql.yml
│       ├── db_ops.yml
│       ├── dev-main-deploy.yml
│       ├── feature-branch-deploy.yml
│       ├── pip-audit.yml
│       ├── prod-deploy.yml
│       ├── renew_coverage.yml
│       ├── stage-hotfix-rel-deploy.yml
│       ├── stage-image-refresh.yml
│       ├── stage-release-deploy.yml
│       ├── test-main-deploy.yml
│       └── test.yml
├── .gitignore
├── Dockerfile
├── LICENSE.md
├── Loadtesting.md
├── MANIFEST.in
├── Makefile
├── Pipfile
├── README.md
├── build_containers.sh
├── celerybeat/
│   ├── Dockerfile
│   └── entrypoint.sh
├── cloudformation/
│   ├── LICENSE
│   ├── NOTICE
│   ├── README.md
│   ├── add_cloudflare_ips_to_sgs.py
│   ├── create_secrets.sh
│   ├── featurebranch.yaml
│   ├── images/
│   │   └── architecture-overview.graffle/
│   │       └── data.plist
│   ├── infrastructure/
│   │   ├── bastion-hosts.yaml
│   │   ├── data-load.yaml
│   │   ├── elasticache-feature.yaml
│   │   ├── elasticache.yaml
│   │   ├── elasticsearch.yaml
│   │   ├── fargate-cluster.yaml
│   │   ├── fargate-featurebranch.yaml
│   │   ├── jenkins-server.yaml
│   │   ├── network-acl.yaml
│   │   ├── opensearch.yaml
│   │   ├── rds.yaml
│   │   ├── search-proxy-task.yaml
│   │   ├── security-groups.yaml
│   │   └── vpc.yaml
│   ├── master.yaml
│   ├── stack_drift.sh
│   ├── sync_templates.sh
│   └── tests/
│       └── validate-templates.sh
├── concordia/
│   ├── __init__.py
│   ├── admin/
│   │   ├── __init__.py
│   │   ├── actions.py
│   │   ├── filters.py
│   │   ├── forms.py
│   │   ├── utils.py
│   │   └── views.py
│   ├── admin_site.py
│   ├── api/
│   │   ├── __init__.py
│   │   └── schemas.py
│   ├── api_views.py
│   ├── apps.py
│   ├── asgi.py
│   ├── authentication_backends.py
│   ├── celery.py
│   ├── consumers.py
│   ├── context_processors.py
│   ├── contextmanagers.py
│   ├── converters.py
│   ├── decorators.py
│   ├── documents.py
│   ├── exceptions.py
│   ├── forms.py
│   ├── logging.py
│   ├── maintenance.py
│   ├── management/
│   │   ├── __init__.py
│   │   └── commands/
│   │       ├── __init__.py
│   │       ├── calculate_difficulty_values.py
│   │       ├── create_load_test_fixtures.py
│   │       ├── ensure_initial_site_configuration.py
│   │       ├── import_site_reports.py
│   │       ├── prepare_load_test_db.py
│   │       └── print_frontend_test_urls.py
│   ├── middleware.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   ├── 0001_squashed_0040_remove_campaign_is_active.py
│   │   ├── 0002_auto_20181004_1848.py
│   │   ├── 0003_auto_20181004_2103.py
│   │   ├── 0004_auto_20181010_1715.py
│   │   ├── 0005_campaign_short_description.py
│   │   ├── 0006_campaignresource.py
│   │   ├── 0007_thumbnail_images.py
│   │   ├── 0008_auto_20181015_1711.py
│   │   ├── 0009_project_description.py
│   │   ├── 0010_auto_20181021_1659.py
│   │   ├── 0010_auto_20181022_1530.py
│   │   ├── 0011_auto_20181022_1532.py
│   │   ├── 0012_merge_20181022_1554.py
│   │   ├── 0013_auto_20181031_1305.py
│   │   ├── 0014_auto_20181115_1411.py
│   │   ├── 0015_auto_20181115_1436.py
│   │   ├── 0016_auto_20181115_1803.py
│   │   ├── 0017_change_transcription_supersedes_related_name.py
│   │   ├── 0018_auto_20181128_1611.py
│   │   ├── 0018_simplepage.py
│   │   ├── 0019_merge_20181128_1715.py
│   │   ├── 0020_auto_20181128_1718.py
│   │   ├── 0021_sitereport.py
│   │   ├── 0022_auto_20181211_1310.py
│   │   ├── 0023_auto_20190130_1555.py
│   │   ├── 0024_add_site_report_ordering.py
│   │   ├── 0024_auto_20190211_1420.py
│   │   ├── 0025_auto_20190329_1705.py
│   │   ├── 0025_unicode_slugs.py
│   │   ├── 0026_update_published_field_definition.py
│   │   ├── 0027_merge_20190423_1657.py
│   │   ├── 0028_asset_year.py
│   │   ├── 0029_assettranscriptionreservation_reservation_token.py
│   │   ├── 0030_auto_20190503_1559.py
│   │   ├── 0031_auto_20190509_1142.py
│   │   ├── 0032_topic_ordering.py
│   │   ├── 0033_simple_content_blocks.py
│   │   ├── 0034_auto_20190627_1438.py
│   │   ├── 0035_auto_20190627_1455.py
│   │   ├── 0036_auto_20190703_1203.py
│   │   ├── 0037_carouselslide.py
│   │   ├── 0038_sitereport_topic.py
│   │   ├── 0039_auto_20200129_1536.py
│   │   ├── 0040_auto_20200130_1756.py
│   │   ├── 0041_auto_20200203_1351.py
│   │   ├── 0042_auto_20200316_1623.py
│   │   ├── 0043_auto_20200323_1729.py
│   │   ├── 0044_auto_20200323_1827.py
│   │   ├── 0045_auto_20200323_1832.py
│   │   ├── 0046_auto_20200323_1907.py
│   │   ├── 0047_auto_20200324_1103.py
│   │   ├── 0048_auto_20200324_1820.py
│   │   ├── 0049_auto_20200324_2004.py
│   │   ├── 0050_auto_20210920_1544.py
│   │   ├── 0051_asset_storage_image.py
│   │   ├── 0052_auto_20220531_1331.py
│   │   ├── 0053_banner.py
│   │   ├── 0054_banner_active.py
│   │   ├── 0055_campaign_status.py
│   │   ├── 0056_auto_20220922_1508.py
│   │   ├── 0057_resource_resource_type.py
│   │   ├── 0058_banner_slug.py
│   │   ├── 0059_resourcefile.py
│   │   ├── 0060_alter_resourcefile_resource.py
│   │   ├── 0061_auto_20230201_1453.py
│   │   ├── 0061_sitereport_registered_contributors.py
│   │   ├── 0062_resourcefile_updated_on.py
│   │   ├── 0062_userretiredcampaign.py
│   │   ├── 0063_banner_alert_status.py
│   │   ├── 0064_alter_banner_alert_status.py
│   │   ├── 0065_alter_userretiredcampaign_unique_together.py
│   │   ├── 0066_auto_20230217_1302.py
│   │   ├── 0066_campaignretirementprogress.py
│   │   ├── 0067_alter_campaignretirementprogress_campaign.py
│   │   ├── 0068_campaignretirementprogress_complete.py
│   │   ├── 0069_merge_20230224_1446.py
│   │   ├── 0070_alter_campaign_options.py
│   │   ├── 0071_auto_20230306_1456.py
│   │   ├── 0072_merge_20230313_1047.py
│   │   ├── 0073_auto_20230314_1327.py
│   │   ├── 0074_auto_20230314_1341.py
│   │   ├── 0075_auto_20230327_1333.py
│   │   ├── 0076_sitereport_report_name.py
│   │   ├── 0077_alter_sitereport_report_name.py
│   │   ├── 0078_alter_sitereport_report_name.py
│   │   ├── 0079_auto_20230601_1234.py
│   │   ├── 0080_auto_20230602_0920.py
│   │   ├── 0081_sitereport_review_actions.py
│   │   ├── 0082_delete_userretiredcampaign.py
│   │   ├── 0083_sitereport_daily_active_users.py
│   │   ├── 0084_rename_review_actions_sitereport_daily_review_actions.py
│   │   ├── 0085_auto_20231016_1432.py
│   │   ├── 0086_auto_20231215_1311.py
│   │   ├── 0087_auto_20240213_0756.py
│   │   ├── 0088_alter_simplepage_body.py
│   │   ├── 0089_campaign_image_alt_text.py
│   │   ├── 0090_auto_20240408_1334.py
│   │   ├── 0091_guide_simple_page.py
│   │   ├── 0092_auto_20240509_1522.py
│   │   ├── 0093_asset_campaign.py
│   │   ├── 0094_alter_asset_campaign.py
│   │   ├── 0095_transcription_rolled_back_and_more.py
│   │   ├── 0096_transcription_source.py
│   │   ├── 0097_alter_sitereport_options_userprofile_review_count_and_more.py
│   │   ├── 0098_userprofile_create_and_population.py
│   │   ├── 0099_alter_campaign_display_on_homepage_and_more.py
│   │   ├── 0100_researchcenter.py
│   │   ├── 0101_auto_20241119_1215.py
│   │   ├── 0102_campaign_research_centers.py
│   │   ├── 0103_alter_item_title.py
│   │   ├── 0104_nexttranscribabletopicasset_and_more.py
│   │   ├── 0105_nextreviewablecampaignasset_concordia_n_transcr_aafdba_gin_and_more.py
│   │   ├── 0106_alter_nextreviewablecampaignasset_options_and_more.py
│   │   ├── 0107_alter_nextreviewablecampaignasset_options_and_more.py
│   │   ├── 0108_add_next_asset_cache_periodic_task.py
│   │   ├── 0109_alter_nextreviewablecampaignasset_asset_and_more.py
│   │   ├── 0110_remove_asset_media_url_alter_asset_storage_image.py
│   │   ├── 0111_auto_20250428_1023.py
│   │   ├── 0112_projecttopic_url_filter_alter_projecttopic_id.py
│   │   ├── 0113_create_asset_status_periodic_task.py
│   │   ├── 0114_create_daily_activity_periodic_task.py
│   │   ├── 0115_alter_asset_storage_image_alter_banner_link_and_more.py
│   │   ├── 0116_item_thumbnail_image.py
│   │   ├── 0117_alter_projecttopic_options_projecttopic_ordering.py
│   │   ├── 0118_asset_concordia_a_item_id_f10916_idx_and_more.py
│   │   ├── 0119_remove_asset_concordia_a_id_137ca8_idx_and_more.py
│   │   ├── 0120_sitereport_assets_started.py
│   │   ├── 0121_keymetricsreport.py
│   │   ├── 0122_alter_item_title.py
│   │   ├── 0123_alter_campaignretirementprogress_options.py
│   │   ├── 0124_update_periodic_task_paths.py
│   │   ├── 0125_update_userprofile_tasks.py
│   │   ├── 0126_concordiafile_helpfullink_remove_resource_campaign_and_more.py
│   │   ├── 0127_alter_campaignretirementprogress_options_and_more.py
│   │   ├── 0128_alter_campaignretirementprogress_options.py
│   │   └── __init__.py
│   ├── models.py
│   ├── parser.py
│   ├── passwords/
│   │   ├── LICENSE
│   │   ├── __init__.py
│   │   └── validators.py
│   ├── routing.py
│   ├── secrets.py
│   ├── settings_dev.py
│   ├── settings_docker.py
│   ├── settings_ecs.py
│   ├── settings_loadtest.py
│   ├── settings_local_test.py
│   ├── settings_template.py
│   ├── settings_test.py
│   ├── signals/
│   │   ├── __init__.py
│   │   ├── handlers.py
│   │   └── signals.py
│   ├── static/
│   │   ├── admin/
│   │   │   ├── custom-inline.js
│   │   │   └── editor-preview.js
│   │   ├── js/
│   │   │   └── src/
│   │   │       ├── about-accordions.js
│   │   │       ├── asset-reservation.js
│   │   │       ├── banner.js
│   │   │       ├── base.js
│   │   │       ├── campaign-selection.js
│   │   │       ├── contribute.js
│   │   │       ├── filter-assets.js
│   │   │       ├── guide.js
│   │   │       ├── homepage-carousel.js
│   │   │       ├── modules/
│   │   │       │   ├── accessible-colors.js
│   │   │       │   ├── chroma-esm.js
│   │   │       │   ├── concordia-visualization.js
│   │   │       │   ├── quick-tips.js
│   │   │       │   ├── turnstile.js
│   │   │       │   └── visualization-errors.js
│   │   │       ├── ocr.js
│   │   │       ├── password-validation.js
│   │   │       ├── profile-fields.js
│   │   │       ├── quick-tips-setup.js
│   │   │       ├── recent-pages.js
│   │   │       ├── viewer-split.js
│   │   │       ├── viewer.js
│   │   │       └── visualizations/
│   │   │           ├── asset-status-by-campaign.js
│   │   │           ├── asset-status-overview.js
│   │   │           └── daily-activity.js
│   │   ├── scss/
│   │   │   ├── _variables.scss
│   │   │   └── base.scss
│   │   └── vendor/
│   │       └── jquery.cookie.js
│   ├── storage.py
│   ├── storage_backends.py
│   ├── tasks/
│   │   ├── __init__.py
│   │   ├── assets.py
│   │   ├── blog.py
│   │   ├── housekeeping.py
│   │   ├── next_asset/
│   │   │   ├── __init__.py
│   │   │   ├── renew.py
│   │   │   ├── reviewable.py
│   │   │   └── transcribable.py
│   │   ├── reports/
│   │   │   ├── __init__.py
│   │   │   ├── backfill.py
│   │   │   ├── key_metrics.py
│   │   │   └── sitereport.py
│   │   ├── reservations.py
│   │   ├── retirement.py
│   │   ├── search_index.py
│   │   ├── thumbnails.py
│   │   ├── unusualactivity.py
│   │   ├── useractivity.py
│   │   └── visualizations.py
│   ├── templates/
│   │   ├── 404.html
│   │   ├── 429.html
│   │   ├── 500.html
│   │   ├── 503.html
│   │   ├── account/
│   │   │   ├── account_deletion.html
│   │   │   ├── email_reconfirmation_failed.html
│   │   │   └── profile.html
│   │   ├── admin/
│   │   │   ├── auth/
│   │   │   │   └── user/
│   │   │   │       └── change_form.html
│   │   │   ├── base_site.html
│   │   │   ├── bulk_change.html
│   │   │   ├── bulk_import.html
│   │   │   ├── bulk_review.html
│   │   │   ├── celery_task.html
│   │   │   ├── clear_cache.html
│   │   │   ├── concordia/
│   │   │   │   ├── asset/
│   │   │   │   │   ├── change_form.html
│   │   │   │   │   └── change_list.html
│   │   │   │   ├── campaign/
│   │   │   │   │   ├── change_form.html
│   │   │   │   │   └── retire.html
│   │   │   │   ├── item/
│   │   │   │   │   └── change_form.html
│   │   │   │   ├── project/
│   │   │   │   │   ├── change_form.html
│   │   │   │   │   └── item_import.html
│   │   │   │   ├── simplepage/
│   │   │   │   │   └── change_form.html
│   │   │   │   └── transcription/
│   │   │   │       └── change_form.html
│   │   │   ├── index.html
│   │   │   ├── long_name_filter.html
│   │   │   ├── process_bagit.html
│   │   │   └── project_level_export.html
│   │   ├── base.html
│   │   ├── django_registration/
│   │   │   ├── activation_complete.html
│   │   │   ├── activation_email_body.txt
│   │   │   ├── activation_email_subject.txt
│   │   │   ├── activation_failed.html
│   │   │   ├── registration_closed.html
│   │   │   ├── registration_complete.html
│   │   │   └── registration_form.html
│   │   ├── documents/
│   │   │   └── service_letter.html
│   │   ├── emails/
│   │   │   ├── delete_account_body.txt
│   │   │   ├── delete_account_subject.txt
│   │   │   ├── email_reconfirmation_body.txt
│   │   │   ├── email_reconfirmation_subject.txt
│   │   │   ├── unusual_activity.html
│   │   │   ├── unusual_activity.txt
│   │   │   ├── welcome_email_body.html
│   │   │   ├── welcome_email_body.txt
│   │   │   └── welcome_email_subject.txt
│   │   ├── error.html
│   │   ├── forms/
│   │   │   └── widgets/
│   │   │       ├── email.html
│   │   │       └── turnstile_widget.html
│   │   ├── fragments/
│   │   │   ├── _filter-buttons.html
│   │   │   ├── _modal_footer.html
│   │   │   ├── activity-filter-sort.html
│   │   │   ├── codemirror.html
│   │   │   ├── common-stylesheets.html
│   │   │   ├── featured_blog_posts.html
│   │   │   ├── recent-pages.html
│   │   │   ├── sharing-button-group.html
│   │   │   ├── standard-pagination.html
│   │   │   ├── transcription-progress-bar.html
│   │   │   ├── transcription-progress-row.html
│   │   │   └── transcription-status-filters.html
│   │   ├── home.html
│   │   ├── registration/
│   │   │   ├── activate.html
│   │   │   ├── login.html
│   │   │   ├── password_change_done.html
│   │   │   ├── password_change_form.html
│   │   │   ├── password_reset_complete.html
│   │   │   ├── password_reset_confirm.html
│   │   │   ├── password_reset_done.html
│   │   │   ├── password_reset_email.html
│   │   │   ├── password_reset_form.html
│   │   │   └── password_reset_subject.txt
│   │   ├── static-page.html
│   │   └── transcriptions/
│   │       ├── asset_detail/
│   │       │   ├── asset_reservation_failure_modal.html
│   │       │   ├── editor.html
│   │       │   ├── error_modal.html
│   │       │   ├── guide.html
│   │       │   ├── language_selection_modal.html
│   │       │   ├── navigation.html
│   │       │   ├── nothing_to_transcribe_modal.html
│   │       │   ├── ocr_help_modal.html
│   │       │   ├── ocr_transcription_modal.html
│   │       │   ├── quick_tips_modal.html
│   │       │   ├── review_accepted_modal.html
│   │       │   ├── successful_submission_modal.html
│   │       │   ├── tags.html
│   │       │   ├── viewer.html
│   │       │   └── viewer_filters.html
│   │       ├── asset_detail.html
│   │       ├── campaign_detail.html
│   │       ├── campaign_detail_completed.html
│   │       ├── campaign_detail_retired.html
│   │       ├── campaign_list.html
│   │       ├── campaign_list_small_blocks.html
│   │       ├── campaign_report.html
│   │       ├── campaign_small_block.html
│   │       ├── campaign_topic_list.html
│   │       ├── completed_campaigns_section.html
│   │       ├── item_detail.html
│   │       ├── project_detail.html
│   │       ├── topic_detail.html
│   │       └── transcription.html
│   ├── templatetags/
│   │   ├── __init__.py
│   │   ├── concordia_filtering_tags.py
│   │   ├── concordia_media_tags.py
│   │   ├── concordia_querystring.py
│   │   ├── concordia_sharing_tags.py
│   │   ├── concordia_text_tags.py
│   │   ├── custom_math.py
│   │   ├── group_list.py
│   │   ├── reject_filter.py
│   │   ├── truncation.py
│   │   └── visualization.py
│   ├── tests/
│   │   ├── README.md
│   │   ├── __init__.py
│   │   ├── axe.py
│   │   ├── data/
│   │   │   └── site_reports.csv
│   │   ├── test_account_views.py
│   │   ├── test_admin.py
│   │   ├── test_admin_actions.py
│   │   ├── test_admin_filters.py
│   │   ├── test_admin_forms.py
│   │   ├── test_admin_views.py
│   │   ├── test_api_views.py
│   │   ├── test_authentication.py
│   │   ├── test_celery.py
│   │   ├── test_consumers.py
│   │   ├── test_contextmanagers.py
│   │   ├── test_decorators.py
│   │   ├── test_fields.py
│   │   ├── test_logging.py
│   │   ├── test_maintenance.py
│   │   ├── test_management_commands.py
│   │   ├── test_models.py
│   │   ├── test_parser.py
│   │   ├── test_registration_views.py
│   │   ├── test_s3.py
│   │   ├── test_selenium.py
│   │   ├── test_sentry.py
│   │   ├── test_signals.py
│   │   ├── test_tasks_assets.py
│   │   ├── test_tasks_blog.py
│   │   ├── test_tasks_housekeeping.py
│   │   ├── test_tasks_next_asset.py
│   │   ├── test_tasks_reports_backfill.py
│   │   ├── test_tasks_reports_key_metrics.py
│   │   ├── test_tasks_reports_sitereport.py
│   │   ├── test_tasks_retirement.py
│   │   ├── test_tasks_search_index.py
│   │   ├── test_tasks_thumbnails.py
│   │   ├── test_tasks_unusualactivity.py
│   │   ├── test_tasks_useractivity.py
│   │   ├── test_tasks_visualizations.py
│   │   ├── test_templatetags.py
│   │   ├── test_top_level_views.py
│   │   ├── test_utils_celery.py
│   │   ├── test_utils_logging.py
│   │   ├── test_utils_next_asset_reviewable_campaign.py
│   │   ├── test_utils_next_asset_reviewable_topic.py
│   │   ├── test_utils_next_asset_transcribable_campaign.py
│   │   ├── test_utils_next_asset_transcribable_topic.py
│   │   ├── test_validators.py
│   │   ├── test_view_decorators.py
│   │   ├── test_views.py
│   │   ├── test_views_asset_reservation.py
│   │   ├── test_views_redirect_next_reviewable.py
│   │   ├── test_views_redirect_next_transcribable.py
│   │   ├── test_views_tags.py
│   │   ├── test_views_topics.py
│   │   ├── test_views_transcription_review.py
│   │   ├── test_views_transcription_save.py
│   │   ├── test_views_transcription_submit.py
│   │   ├── test_views_utils.py
│   │   ├── test_widgets.py
│   │   └── utils.py
│   ├── turnstile/
│   │   ├── LICENSE
│   │   ├── __init__.py
│   │   ├── context_processors.py
│   │   ├── fields.py
│   │   └── widgets.py
│   ├── urls.py
│   ├── utils/
│   │   ├── __init__.py
│   │   ├── celery.py
│   │   ├── constants.py
│   │   └── next_asset/
│   │       ├── __init__.py
│   │       ├── reviewable/
│   │       │   ├── __init__.py
│   │       │   ├── campaign.py
│   │       │   └── topic.py
│   │       └── transcribable/
│   │           ├── __init__.py
│   │           ├── campaign.py
│   │           └── topic.py
│   ├── validators.py
│   ├── version.py
│   ├── views/
│   │   ├── README.md
│   │   ├── __init__.py
│   │   ├── accounts.py
│   │   ├── ajax.py
│   │   ├── assets.py
│   │   ├── campaigns.py
│   │   ├── decorators.py
│   │   ├── items.py
│   │   ├── maintenance_mode.py
│   │   ├── projects.py
│   │   ├── rate_limit.py
│   │   ├── simple_pages.py
│   │   ├── topics.py
│   │   ├── utils.py
│   │   └── visualizations.py
│   ├── widgets.py
│   └── wsgi.py
├── configuration/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── management/
│   │   ├── __init__.py
│   │   └── commands/
│   │       ├── __init__.py
│   │       └── configcache.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   ├── 0002_populate_configurations.py
│   │   ├── 0003_populate_retry_configurations.py
│   │   ├── 0004_alter_configuration_options.py
│   │   ├── 0005_alter_configuration_data_type.py
│   │   ├── 0006_populate_next_asset_rate_limit.py
│   │   └── __init__.py
│   ├── models.py
│   ├── signals.py
│   ├── templates/
│   │   └── admin/
│   │       └── configuration_confirm_update.html
│   ├── templatetags/
│   │   ├── __init__.py
│   │   └── configuration_tags.py
│   ├── tests/
│   │   ├── __init__.py
│   │   ├── test_admin.py
│   │   ├── test_models.py
│   │   ├── test_signals.py
│   │   ├── test_templatetags.py
│   │   ├── test_utils.py
│   │   └── test_validation.py
│   ├── utils.py
│   ├── validation.py
│   └── views.py
├── db_scripts/
│   ├── Dockerfile
│   ├── dump.sh
│   └── restore.sh
├── development/
│   ├── Containerfile
│   ├── README.md
│   └── compose.yml
├── docker-compose.yml
├── docs/
│   ├── accessibility-goals.md
│   ├── accessibility-techniques.md
│   ├── design-principles.md
│   ├── for-developers.md
│   └── how-we-work.md
├── entrypoint.sh
├── exporter/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── exceptions.py
│   ├── migrations/
│   │   └── __init__.py
│   ├── models.py
│   ├── tabular_export/
│   │   ├── admin.py
│   │   └── core.py
│   ├── templates/
│   │   └── admin/
│   │       └── exporter/
│   │           └── unacceptable_character_report.html
│   ├── tests/
│   │   ├── __init__.py
│   │   ├── test_exceptions.py
│   │   ├── test_tabular_export.py
│   │   ├── test_utils.py
│   │   └── test_views.py
│   ├── utils.py
│   └── views.py
├── fixtures/
│   └── original-static-pages.json
├── frontend/
│   ├── .gitignore
│   ├── README.md
│   ├── eslint.config.js
│   ├── index.html
│   ├── package.json
│   ├── src/
│   │   ├── App.jsx
│   │   ├── ViewerSplit.jsx
│   │   ├── config.js
│   │   ├── editor/
│   │   │   ├── Buttons.jsx
│   │   │   ├── Editor.jsx
│   │   │   ├── Header.jsx
│   │   │   ├── StatusMessages.jsx
│   │   │   ├── TranscriptionTextarea.jsx
│   │   │   └── buttons/
│   │   │       ├── Editable.jsx
│   │   │       ├── Redo.jsx
│   │   │       ├── Review.jsx
│   │   │       ├── Save.jsx
│   │   │       ├── Submit.jsx
│   │   │       └── Undo.jsx
│   │   ├── main.jsx
│   │   ├── ocr/
│   │   │   ├── Button.jsx
│   │   │   ├── ConfirmModal.jsx
│   │   │   ├── Handler.jsx
│   │   │   ├── HelpModal.jsx
│   │   │   ├── LanguageModal.jsx
│   │   │   └── Section.jsx
│   │   └── viewer/
│   │       ├── Controls.jsx
│   │       ├── FilterTabNav.jsx
│   │       ├── GammaFilterForm.jsx
│   │       ├── ImageFilters.jsx
│   │       ├── InvertFilterForm.jsx
│   │       ├── KeyboardHelpModal.jsx
│   │       ├── KeyboardShortcutRow.jsx
│   │       ├── ThresholdFilterForm.jsx
│   │       └── Viewer.jsx
│   └── vite.config.js
├── importer/
│   ├── Dockerfile
│   ├── README.md
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── celery.py
│   ├── config.py
│   ├── entrypoint.sh
│   ├── exceptions.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   ├── 0001_squashed_0015_auto_20180925_1851.py
│   │   ├── 0002_auto_20180709_0833.py
│   │   ├── 0003_auto_20180709_0933.py
│   │   ├── 0004_auto_20180812_1007.py
│   │   ├── 0005_auto_20180816_1702.py
│   │   ├── 0006_auto_20180912_0229.py
│   │   ├── 0007_auto_20180917_1654.py
│   │   ├── 0008_campaigntaskdetails_project.py
│   │   ├── 0009_convert_project_text_to_keys.py
│   │   ├── 0010_auto_20180920_2013.py
│   │   ├── 0011_auto_20180922_0208.py
│   │   ├── 0012_auto_20180923_0231.py
│   │   ├── 0013_auto_20180924_1318.py
│   │   ├── 0014_auto_20180924_1943.py
│   │   ├── 0015_auto_20180925_1851.py
│   │   ├── 0016_importitem_failure_reason_and_more.py
│   │   ├── 0017_importitem_failure_history_importitem_retry_count_and_more.py
│   │   ├── 0018_importitem_status_history_and_more.py
│   │   ├── 0019_alter_downloadassetimagejob_batch_and_more.py
│   │   ├── 0020_alter_downloadassetimagejob_unique_together_and_more.py
│   │   └── __init__.py
│   ├── models.py
│   ├── setup.py
│   ├── tasks/
│   │   ├── __init__.py
│   │   ├── assets.py
│   │   ├── collections.py
│   │   ├── decorators.py
│   │   ├── images.py
│   │   └── items.py
│   ├── tests/
│   │   ├── README.md
│   │   ├── __init__.py
│   │   ├── test_admin.py
│   │   ├── test_celery.py
│   │   ├── test_models.py
│   │   ├── test_tasks_assets.py
│   │   ├── test_tasks_collections.py
│   │   ├── test_tasks_core.py
│   │   ├── test_tasks_decorators.py
│   │   ├── test_tasks_images.py
│   │   ├── test_tasks_items.py
│   │   ├── test_utils.py
│   │   └── utils.py
│   └── utils/
│       ├── __init__.py
│       ├── excel.py
│       └── verify_images.py
├── load_test.sh
├── locustfile.py
├── manage.py
├── package.json
├── postgresql/
│   └── create-multiple-postgresql-databases.sh
├── prometheus_metrics/
│   ├── LICENSE
│   ├── __init__.py
│   ├── apps.py
│   ├── middleware.py
│   ├── models.py
│   └── views.py
├── pylenium.json
├── pyproject.toml
├── setup.cfg
├── setup.py
├── src/
│   ├── about.js
│   ├── main.js
│   └── profile.js
├── static/
│   └── .gitignore
├── tools/
│   └── readme_symbol_check.py
└── vite.config.js
Download .txt
Showing preview only (275K chars total). Download the full file or copy to clipboard to get everything.
SYMBOL INDEX (2770 symbols across 415 files)

FILE: cloudformation/add_cloudflare_ips_to_sgs.py
  function add_ingess_rules_for_group (line 23) | def add_ingess_rules_for_group(sg_id, existing_permissions):
  function get_security_groups (line 60) | def get_security_groups():

FILE: concordia/admin/__init__.py
  class ConcordiaUserAdmin (line 119) | class ConcordiaUserAdmin(UserAdmin):
    method get_queryset (line 136) | def get_queryset(
    method transcription_count (line 159) | def transcription_count(self, obj: User) -> int:
    method review_count (line 166) | def review_count(self, obj: User) -> int:
    method export_users_as_csv (line 188) | def export_users_as_csv(
    method export_users_as_excel (line 211) | def export_users_as_excel(
  class BannerAdmin (line 242) | class BannerAdmin(admin.ModelAdmin):
  class CustomListDisplayFieldsMixin (line 249) | class CustomListDisplayFieldsMixin:
    method truncated_description (line 258) | def truncated_description(self, obj):
    method truncated_metadata (line 262) | def truncated_metadata(self, obj):
  class CampaignAdmin (line 270) | class CampaignAdmin(admin.ModelAdmin, CustomListDisplayFieldsMixin):
    method get_form (line 338) | def get_form(
    method get_urls (line 368) | def get_urls(self):
    method retire (line 425) | def retire(
    method log_retirement (line 504) | def log_retirement(
  class HelpfulLinkAdmin (line 532) | class HelpfulLinkAdmin(admin.ModelAdmin, CustomListDisplayFieldsMixin):
    method formfield_for_foreignkey (line 542) | def formfield_for_foreignkey(
  class ConcordiaFileAdmin (line 568) | class ConcordiaFileAdmin(admin.ModelAdmin):
    method file_url (line 574) | def file_url(self, obj: ConcordiaFile) -> str:
    method get_fields (line 595) | def get_fields(
  class TopicProjectInline (line 624) | class TopicProjectInline(admin.TabularInline):
  class TopicAdmin (line 634) | class TopicAdmin(admin.ModelAdmin):
  class ProjectTopicInline (line 656) | class ProjectTopicInline(admin.TabularInline):
  class ProjectAdmin (line 665) | class ProjectAdmin(admin.ModelAdmin, CustomListDisplayFieldsMixin):
    method lookup_allowed (line 684) | def lookup_allowed(self, key: str, value: str) -> bool:
    method get_urls (line 700) | def get_urls(self):
    method item_import_view (line 743) | def item_import_view(
  class ItemAdmin (line 811) | class ItemAdmin(admin.ModelAdmin):
    method lookup_allowed (line 833) | def lookup_allowed(self, key: str, value: str) -> bool:
    method get_deleted_objects (line 849) | def get_deleted_objects(
    method get_queryset (line 898) | def get_queryset(
    method campaign_title (line 912) | def campaign_title(self, obj: Item) -> str:
  class AssetTranscriptionReservationAdmin (line 926) | class AssetTranscriptionReservationAdmin(
  class AssetAdmin (line 941) | class AssetAdmin(admin.ModelAdmin, CustomListDisplayFieldsMixin):
    method get_queryset (line 990) | def get_queryset(
    method lookup_allowed (line 1003) | def lookup_allowed(self, key: str, value: str) -> bool:
    method response_action (line 1019) | def response_action(
    method item_id (line 1058) | def item_id(self, obj: Asset) -> str:
    method truncated_storage_image (line 1062) | def truncated_storage_image(self, obj: Asset) -> str:
    method get_readonly_fields (line 1069) | def get_readonly_fields(
    method change_view (line 1091) | def change_view(
    method has_reopen_permission (line 1147) | def has_reopen_permission(self, request: HttpRequest) -> bool:
  class TagAdmin (line 1163) | class TagAdmin(admin.ModelAdmin):
    method lookup_allowed (line 1172) | def lookup_allowed(self, key: str, value: str) -> bool:
    method export_tags_as_csv (line 1187) | def export_tags_as_csv(
  class UserAssetTagCollectionAdmin (line 1234) | class UserAssetTagCollectionAdmin(admin.ModelAdmin):
  class TranscriptionAdmin (line 1248) | class TranscriptionAdmin(admin.ModelAdmin):
    method get_queryset (line 1310) | def get_queryset(
    method truncated_text (line 1338) | def truncated_text(self, obj: Transcription) -> str:
    method superseded (line 1351) | def superseded(self, obj: Transcription) -> bool:
    method lookup_allowed (line 1367) | def lookup_allowed(self, key: str, value: str) -> bool:
    method export_to_csv (line 1382) | def export_to_csv(
    method export_to_excel (line 1401) | def export_to_excel(
  class CarouselSlideAdmin (line 1424) | class CarouselSlideAdmin(admin.ModelAdmin):
  class SimplePageAdmin (line 1430) | class SimplePageAdmin(admin.ModelAdmin):
  class SiteReportAdmin (line 1441) | class SiteReportAdmin(admin.ModelAdmin):
    method report_type (line 1500) | def report_type(self, obj: "SiteReport") -> str:
    method report_json (line 1520) | def report_json(self, obj: "SiteReport") -> str:
    method previous_in_series_link (line 1536) | def previous_in_series_link(self, obj: "SiteReport") -> str:
    method next_in_series_link (line 1558) | def next_in_series_link(self, obj: "SiteReport") -> str:
    method export_to_csv (line 1579) | def export_to_csv(
    method export_to_excel (line 1598) | def export_to_excel(
  class UserProfileActivityAdmin (line 1621) | class UserProfileActivityAdmin(admin.ModelAdmin):
  class CampaignRetirementProgressAdmin (line 1649) | class CampaignRetirementProgressAdmin(admin.ModelAdmin):
    method completion (line 1694) | def completion(self, obj: CampaignRetirementProgress) -> str:
  class CardAdmin (line 1712) | class CardAdmin(admin.ModelAdmin):
  class TutorialInline (line 1719) | class TutorialInline(admin.TabularInline):
  class CardFamilyAdmin (line 1726) | class CardFamilyAdmin(admin.ModelAdmin):
    class Media (line 1729) | class Media:
  class GuideAdmin (line 1734) | class GuideAdmin(admin.ModelAdmin):
  class NextTranscribableCampaignAssetAdmin (line 1739) | class NextTranscribableCampaignAssetAdmin(admin.ModelAdmin):
  class NextReviewableCampaignAssetAdmin (line 1770) | class NextReviewableCampaignAssetAdmin(admin.ModelAdmin):
  class NextTranscribableTopicAssetAdmin (line 1796) | class NextTranscribableTopicAssetAdmin(admin.ModelAdmin):
  class NextReviewableTopicAssetAdmin (line 1827) | class NextReviewableTopicAssetAdmin(admin.ModelAdmin):
  class KeyMetricsReportAdmin (line 1853) | class KeyMetricsReportAdmin(admin.ModelAdmin):
    method download_csv_link (line 1955) | def download_csv_link(self, obj: "KeyMetricsReport") -> str:
    method get_urls (line 1971) | def get_urls(self):
    method download_csv_view (line 1989) | def download_csv_view(
    method download_selected_as_zip (line 2016) | def download_selected_as_zip(

FILE: concordia/admin/actions.py
  function anonymize_action (line 28) | def anonymize_action(
  function publish_item_action (line 68) | def publish_item_action(
  function unpublish_item_action (line 104) | def unpublish_item_action(
  function publish_action (line 137) | def publish_action(
  function unpublish_action (line 162) | def unpublish_action(
  function change_status_to_completed (line 187) | def change_status_to_completed(
  function change_status_to_needs_review (line 246) | def change_status_to_needs_review(
  function change_status_to_in_progress (line 289) | def change_status_to_in_progress(
  function verify_assets_action (line 336) | def verify_assets_action(

FILE: concordia/admin/filters.py
  class NullableTimestampFilter (line 8) | class NullableTimestampFilter(admin.SimpleListFilter):
    method lookups (line 23) | def lookups(self, request, model_admin):
    method queryset (line 26) | def queryset(self, request, queryset):
  class SubmittedFilter (line 35) | class SubmittedFilter(NullableTimestampFilter):
  class AcceptedFilter (line 45) | class AcceptedFilter(NullableTimestampFilter):
  class RejectedFilter (line 55) | class RejectedFilter(NullableTimestampFilter):
  class CampaignListFilter (line 65) | class CampaignListFilter(admin.SimpleListFilter):
    method lookups (line 77) | def lookups(self, request, model_admin):
    method queryset (line 83) | def queryset(self, request, queryset):
  class CardCampaignListFilter (line 89) | class CardCampaignListFilter(admin.SimpleListFilter):
    method lookups (line 100) | def lookups(self, request, model_admin):
    method queryset (line 105) | def queryset(self, request, queryset):
  class TopicListFilter (line 117) | class TopicListFilter(admin.SimpleListFilter):
    method lookups (line 128) | def lookups(self, request, model_admin):
    method queryset (line 132) | def queryset(self, request, queryset):
  class ProjectCampaignListFilter (line 138) | class ProjectCampaignListFilter(CampaignListFilter):
  class ItemCampaignListFilter (line 143) | class ItemCampaignListFilter(CampaignListFilter):
  class AssetCampaignListFilter (line 148) | class AssetCampaignListFilter(CampaignListFilter):
  class UserProfileActivityCampaignListFilter (line 153) | class UserProfileActivityCampaignListFilter(CampaignListFilter):
  class SiteReportCampaignListBaseFilter (line 158) | class SiteReportCampaignListBaseFilter(CampaignListFilter):
    method __init__ (line 170) | def __init__(self, request, params, model, model_admin):
    method has_output (line 175) | def has_output(self):
    method expected_parameters (line 182) | def expected_parameters(self):
    method choices (line 185) | def choices(self, changelist):
  class SiteReportSortedCampaignListFilter (line 211) | class SiteReportSortedCampaignListFilter(SiteReportCampaignListBaseFilter):
  class SiteReportCampaignListFilter (line 217) | class SiteReportCampaignListFilter(SiteReportCampaignListBaseFilter):
    method lookups (line 221) | def lookups(self, request, model_admin):
  class HelpfulLinkCampaignListFilter (line 225) | class HelpfulLinkCampaignListFilter(CampaignListFilter):
  class TagCampaignListFilter (line 231) | class TagCampaignListFilter(CampaignListFilter):
  class TranscriptionCampaignListFilter (line 238) | class TranscriptionCampaignListFilter(CampaignListFilter):
  class UserAssetTagCollectionCampaignListFilter (line 243) | class UserAssetTagCollectionCampaignListFilter(CampaignListFilter):
  class NextAssetCampaignListFilter (line 248) | class NextAssetCampaignListFilter(CampaignListFilter):
    method lookups (line 251) | def lookups(self, request, model_admin):
  class CampaignProjectListFilter (line 260) | class CampaignProjectListFilter(admin.SimpleListFilter):
    method lookups (line 274) | def lookups(self, request, model_admin):
    method queryset (line 285) | def queryset(self, request, queryset):
  class ItemProjectListFilter (line 291) | class ItemProjectListFilter(CampaignProjectListFilter):
  class AssetProjectListFilter (line 297) | class AssetProjectListFilter(CampaignProjectListFilter):
  class TranscriptionProjectListFilter (line 303) | class TranscriptionProjectListFilter(CampaignProjectListFilter):
  class CampaignStatusListFilter (line 309) | class CampaignStatusListFilter(admin.SimpleListFilter):
    method lookups (line 319) | def lookups(self, request, model_admin):
    method queryset (line 322) | def queryset(self, request, queryset):
  class AssetCampaignStatusListFilter (line 328) | class AssetCampaignStatusListFilter(CampaignStatusListFilter):
  class ItemCampaignStatusListFilter (line 332) | class ItemCampaignStatusListFilter(CampaignStatusListFilter):
  class ProjectCampaignStatusListFilter (line 336) | class ProjectCampaignStatusListFilter(CampaignStatusListFilter):
  class HelpfulLinkCampaignStatusListFilter (line 340) | class HelpfulLinkCampaignStatusListFilter(CampaignStatusListFilter):
  class TranscriptionCampaignStatusListFilter (line 344) | class TranscriptionCampaignStatusListFilter(CampaignStatusListFilter):
  class TagCampaignStatusListFilter (line 348) | class TagCampaignStatusListFilter(CampaignStatusListFilter):
  class UserAssetTagCollectionCampaignStatusListFilter (line 352) | class UserAssetTagCollectionCampaignStatusListFilter(CampaignStatusListF...
  class UserProfileActivityCampaignStatusListFilter (line 356) | class UserProfileActivityCampaignStatusListFilter(CampaignStatusListFilt...
  class BooleanFilter (line 360) | class BooleanFilter(admin.SimpleListFilter):
    method lookups (line 368) | def lookups(self, request, model_admin):
    method queryset (line 374) | def queryset(self, request, queryset):
  class OcrGeneratedFilter (line 381) | class OcrGeneratedFilter(BooleanFilter):
  class OcrOriginatedFilter (line 386) | class OcrOriginatedFilter(BooleanFilter):
  class SupersededListFilter (line 391) | class SupersededListFilter(admin.SimpleListFilter):
    method lookups (line 402) | def lookups(self, request, model_admin):
    method queryset (line 405) | def queryset(self, request, queryset):

FILE: concordia/admin/forms.py
  class AdminItemImportForm (line 59) | class AdminItemImportForm(forms.Form):
  class AdminProjectBulkImportForm (line 72) | class AdminProjectBulkImportForm(forms.Form):
  class AdminAssetsBulkChangeStatusForm (line 91) | class AdminAssetsBulkChangeStatusForm(forms.Form):
  class SanitizedDescriptionAdminForm (line 103) | class SanitizedDescriptionAdminForm(forms.ModelForm):
    class Meta (line 112) | class Meta:
    method clean_description (line 116) | def clean_description(self) -> str:
    method clean_short_description (line 129) | def clean_short_description(self) -> str:
  class TopicAdminForm (line 143) | class TopicAdminForm(SanitizedDescriptionAdminForm):
    class Meta (line 148) | class Meta(SanitizedDescriptionAdminForm.Meta):
  class CampaignAdminForm (line 156) | class CampaignAdminForm(SanitizedDescriptionAdminForm):
    class Meta (line 161) | class Meta(SanitizedDescriptionAdminForm.Meta):
  class ProjectAdminForm (line 170) | class ProjectAdminForm(SanitizedDescriptionAdminForm):
    class Meta (line 175) | class Meta(SanitizedDescriptionAdminForm.Meta):
  class ProjectTopicInlineForm (line 182) | class ProjectTopicInlineForm(forms.ModelForm):
    class Meta (line 195) | class Meta:
  class ItemAdminForm (line 200) | class ItemAdminForm(forms.ModelForm):
    class Meta (line 205) | class Meta:
  class CardAdminForm (line 211) | class CardAdminForm(forms.ModelForm):
    class Meta (line 216) | class Meta:
  class GuideAdminForm (line 224) | class GuideAdminForm(forms.ModelForm):
    class Meta (line 229) | class Meta:
  function get_cache_name_choices (line 237) | def get_cache_name_choices() -> list[tuple[str, str]]:
  class ClearCacheForm (line 257) | class ClearCacheForm(forms.Form):
  class AssetStatusActionForm (line 268) | class AssetStatusActionForm(forms.Form):
    method __init__ (line 287) | def __init__(
  class KeyMetricsReportAdminForm (line 311) | class KeyMetricsReportAdminForm(forms.ModelForm):
    class Meta (line 319) | class Meta:

FILE: concordia/admin/utils.py
  function _change_status (line 9) | def _change_status(
  function _bulk_change_status (line 77) | def _bulk_change_status(

FILE: concordia/admin/views.py
  function project_level_export (line 58) | def project_level_export(request: HttpRequest) -> HttpResponse:
  function celery_task_review (line 161) | def celery_task_review(request: HttpRequest) -> HttpResponse:
  function admin_bulk_import_review (line 272) | def admin_bulk_import_review(request: HttpRequest) -> HttpResponse:
  class AdminBulkChangeAssetStatusView (line 393) | class AdminBulkChangeAssetStatusView(FormView):
    method form_valid (line 397) | def form_valid(self, form):
  function admin_bulk_import_view (line 489) | def admin_bulk_import_view(request: HttpRequest) -> HttpResponse:
  function admin_site_report_view (line 656) | def admin_site_report_view(request: HttpRequest) -> HttpResponse:
  function admin_retired_site_report_view (line 682) | def admin_retired_site_report_view(request: HttpRequest) -> HttpResponse:
  class SerializedObjectView (line 725) | class SerializedObjectView(View):
    method get (line 734) | def get(
  class ClearCacheView (line 772) | class ClearCacheView(FormView):
    method form_valid (line 784) | def form_valid(self, form: ClearCacheForm) -> HttpResponse:

FILE: concordia/admin_site.py
  class ConcordiaAdminSite (line 11) | class ConcordiaAdminSite(admin.AdminSite):
    method get_urls (line 17) | def get_urls(self) -> list:

FILE: concordia/api/__init__.py
  class AssetOut (line 70) | class AssetOut(CamelSchema):
  class ReviewIn (line 103) | class ReviewIn(CamelSchema):
  class TranscriptionIn (line 112) | class TranscriptionIn(CamelSchema):
  class OcrTranscriptionIn (line 124) | class OcrTranscriptionIn(CamelSchema):
  class TranscriptionOut (line 136) | class TranscriptionOut(CamelSchema):
  function serialize_asset (line 151) | def serialize_asset(asset: Asset, request: HttpRequest) -> AssetOut:
  function asset_detail_by_slugs (line 291) | def asset_detail_by_slugs(
  function asset_detail (line 324) | def asset_detail(request: HttpRequest, asset_id: int) -> AssetOut:
  function create_transcription (line 333) | def create_transcription(
  function create_ocr_transcription (line 446) | def create_ocr_transcription(
  function rollback (line 558) | def rollback(request: HttpRequest, asset_id: int) -> TranscriptionOut:
  function rollforward (line 605) | def rollforward(request: HttpRequest, asset_id: int) -> TranscriptionOut:
  function submit_transcription (line 650) | def submit_transcription(request: HttpRequest, pk: int) -> Transcription...
  function review_transcription (line 715) | def review_transcription(

FILE: concordia/api/schemas.py
  function to_camel (line 4) | def to_camel(string: str) -> str:
  class CamelSchema (line 19) | class CamelSchema(Schema):
    class Config (line 25) | class Config(Schema.Config):

FILE: concordia/api_views.py
  class URLAwareEncoder (line 26) | class URLAwareEncoder(DjangoJSONEncoder):
    method default (line 32) | def default(self, obj):
  class APIViewMixin (line 46) | class APIViewMixin(TemplateResponseMixin):
    method render_to_response (line 53) | def render_to_response(self, context, **response_kwargs):
    method render_to_json_response (line 62) | def render_to_json_response(self, context):
    method serialize_context (line 67) | def serialize_context(self, context):
    method serialize_object (line 72) | def serialize_object(self, obj):
    method make_absolute_urls (line 78) | def make_absolute_urls(self, data):
  class APIDetailView (line 90) | class APIDetailView(APIViewMixin, DetailView):
    method serialize_context (line 93) | def serialize_context(self, context):
  class APIListView (line 97) | class APIListView(APIViewMixin, ListView):
    method render_to_response (line 100) | def render_to_response(self, context, **response_kwargs):
    method build_url_for_page (line 124) | def build_url_for_page(self, page_number, per_page):
    method get_paginate_by (line 132) | def get_paginate_by(self, queryset):
    method serialize_context (line 140) | def serialize_context(self, context):

FILE: concordia/apps.py
  class ConcordiaAppConfig (line 6) | class ConcordiaAppConfig(AppConfig):
    method ready (line 9) | def ready(self):
  class ConcordiaAdminConfig (line 13) | class ConcordiaAdminConfig(AdminConfig):
    method ready (line 16) | def ready(self):
  class ConcordiaStaticFilesConfig (line 20) | class ConcordiaStaticFilesConfig(StaticFilesConfig):

FILE: concordia/authentication_backends.py
  class EmailOrUsernameModelBackend (line 10) | class EmailOrUsernameModelBackend(ModelBackend):
    method authenticate (line 34) | def authenticate(

FILE: concordia/celery.py
  function import_all_submodules (line 34) | def import_all_submodules(package_name: str):
  function _load_all_task_modules (line 51) | def _load_all_task_modules(sender, **kwargs):

FILE: concordia/consumers.py
  class AssetConsumer (line 6) | class AssetConsumer(AsyncJsonWebsocketConsumer):
    method connect (line 7) | async def connect(self):
    method disconnect (line 11) | async def disconnect(self, code):
    method asset_update (line 14) | async def asset_update(self, message):
    method asset_reservation_obtained (line 17) | async def asset_reservation_obtained(self, message):
    method asset_reservation_released (line 20) | async def asset_reservation_released(self, message):

FILE: concordia/context_processors.py
  function system_configuration (line 8) | def system_configuration(request: HttpRequest) -> Dict[str, Any]:
  function site_navigation (line 34) | def site_navigation(request: HttpRequest) -> Dict[str, Any]:
  function maintenance_mode_frontend_available (line 73) | def maintenance_mode_frontend_available(request: HttpRequest) -> Dict[st...
  function request_id_context (line 92) | def request_id_context(request: HttpRequest) -> Dict[str, Any]:

FILE: concordia/contextmanagers.py
  function cache_lock (line 17) | def cache_lock(

FILE: concordia/converters.py
  class UnicodeSlugConverter (line 4) | class UnicodeSlugConverter(SlugConverter):
  class ItemIdConverter (line 10) | class ItemIdConverter(StringConverter):

FILE: concordia/decorators.py
  function locked_task (line 13) | def locked_task(function=None, lock_by_args: bool = True):

FILE: concordia/documents.py
  class UserDocument (line 11) | class UserDocument(Document):
    class Index (line 12) | class Index:
    class Django (line 20) | class Django:
    method prepare_transcription_count (line 24) | def prepare_transcription_count(self, instance):
  class SiteReportDocument (line 30) | class SiteReportDocument(Document):
    class Index (line 31) | class Index:
    class Django (line 40) | class Django:
  class TagCollectionDocument (line 72) | class TagCollectionDocument(Document):
    class Index (line 73) | class Index:
    class Django (line 102) | class Django:
    method get_queryset (line 106) | def get_queryset(self, *args, **kwargs):
  class TranscriptionDocument (line 118) | class TranscriptionDocument(Document):
    class Index (line 119) | class Index:
    class Django (line 152) | class Django:
    method get_queryset (line 165) | def get_queryset(self, *args, **kwargs):
  class AssetDocument (line 180) | class AssetDocument(Document):
    class Index (line 181) | class Index:
    method prepare_submission_count (line 218) | def prepare_submission_count(self, instance):
    class Django (line 223) | class Django:
    method get_queryset (line 227) | def get_queryset(self, *args, **kwargs):

FILE: concordia/exceptions.py
  class CacheLockedError (line 3) | class CacheLockedError(Exception):
    method __init__ (line 4) | def __init__(self, message, details=None):
  class RateLimitExceededError (line 9) | class RateLimitExceededError(Exception):

FILE: concordia/forms.py
  class AllowInactivePasswordResetForm (line 24) | class AllowInactivePasswordResetForm(PasswordResetForm):
    method get_users (line 34) | def get_users(self, email: str) -> Iterator[User]:
  class ActivateAndSetPasswordForm (line 52) | class ActivateAndSetPasswordForm(SetPasswordForm):
    method save (line 65) | def save(self, commit: bool = True) -> User:
  class UserRegistrationForm (line 86) | class UserRegistrationForm(RegistrationForm):
    class Meta (line 104) | class Meta(RegistrationForm.Meta):
  class UserLoginForm (line 114) | class UserLoginForm(AuthenticationForm):
    method confirm_login_allowed (line 128) | def confirm_login_allowed(self, user: Any) -> None:
  class UserNameForm (line 157) | class UserNameForm(forms.Form):
  class UserProfileForm (line 170) | class UserProfileForm(forms.Form):
    method __init__ (line 180) | def __init__(self, *, request: HttpRequest, **kwargs) -> None:
    method clean_email (line 190) | def clean_email(self) -> str:
  class AccountDeletionForm (line 213) | class AccountDeletionForm(forms.Form):
    method __init__ (line 220) | def __init__(self, *, request: HttpRequest, **kwargs) -> None:
  class TurnstileForm (line 231) | class TurnstileForm(forms.Form):

FILE: concordia/logging.py
  function get_logging_user_id (line 8) | def get_logging_user_id(user: Any) -> str:
  function _register_default_extractor (line 36) | def _register_default_extractor(
  class ConcordiaLogger (line 89) | class ConcordiaLogger:
    method __init__ (line 214) | def __init__(self, logger, context: Optional[dict[str, Any]] = None):
    method get_logger (line 220) | def get_logger(cls, name: str) -> "ConcordiaLogger":
    method register_extractor (line 232) | def register_extractor(
    method unregister_extractor (line 252) | def unregister_extractor(self, key: str) -> None:
    method log (line 261) | def log(
    method debug (line 333) | def debug(self, message: str, *, event_code: str, **kwargs):
    method info (line 337) | def info(self, message: str, *, event_code: str, **kwargs):
    method warning (line 341) | def warning(
    method error (line 354) | def error(
    method exception (line 367) | def exception(
    method bind (line 392) | def bind(self, **kwargs: Any) -> "ConcordiaLogger":

FILE: concordia/maintenance.py
  function _need_maintenence_frontend (line 16) | def _need_maintenence_frontend(request: HttpRequest) -> bool | None:
  function need_maintenance_response (line 42) | def need_maintenance_response(request: HttpRequest) -> bool:

FILE: concordia/management/commands/calculate_difficulty_values.py
  class Command (line 16) | class Command(BaseCommand):
    method handle (line 25) | def handle(self, *, verbosity: int, **kwargs) -> None:

FILE: concordia/management/commands/create_load_test_fixtures.py
  function _serialize_qs (line 31) | def _serialize_qs(qs):
  function _serialize_list (line 35) | def _serialize_list(objs):
  class Command (line 39) | class Command(BaseCommand):
    method add_arguments (line 56) | def add_arguments(self, p):
    method handle (line 117) | def handle(self, *args, **o):

FILE: concordia/management/commands/ensure_initial_site_configuration.py
  class Command (line 36) | class Command(BaseCommand):
    method add_arguments (line 39) | def add_arguments(self, parser: ArgumentParser) -> None:
    method handle (line 69) | def handle(

FILE: concordia/management/commands/import_site_reports.py
  class Command (line 39) | class Command(BaseCommand):
    method add_arguments (line 42) | def add_arguments(self, parser: ArgumentParser) -> None:
    method handle (line 55) | def handle(self, *, csv_file: str, **options) -> None:

FILE: concordia/management/commands/prepare_load_test_db.py
  function _dbinfo (line 12) | def _dbinfo(alias: str):
  function _require_postgres (line 24) | def _require_postgres(engine: str):
  function _maintenance_dsn (line 29) | def _maintenance_dsn(info: dict) -> str:
  function _pg_connect (line 42) | def _pg_connect(dsn: str):
  function _db_exists (line 62) | def _db_exists(cur, name: str) -> bool:
  function _create_db_if_needed (line 67) | def _create_db_if_needed(src_info: dict, name: str, *, recreate: bool = ...
  function _drop_db (line 88) | def _drop_db(src_info: dict, name: str):
  function _switch_process_db (line 104) | def _switch_process_db(alias: str, new_name: str):
  function _suppress_all_django_signals (line 110) | def _suppress_all_django_signals(active: bool):
  class Command (line 139) | class Command(BaseCommand):
    method add_arguments (line 145) | def add_arguments(self, p):
    method handle (line 179) | def handle(self, *args, **o):

FILE: concordia/management/commands/print_frontend_test_urls.py
  class Command (line 24) | class Command(BaseCommand):
    method add_arguments (line 29) | def add_arguments(self, parser: "argparse.ArgumentParser") -> None:
    method handle (line 37) | def handle(self, *, base_url: str, **options) -> None:

FILE: concordia/middleware.py
  class MaintenanceModeMiddleware (line 9) | class MaintenanceModeMiddleware(BaseMaintenanceModeMiddleware):
    method process_request (line 10) | def process_request(self, request):

FILE: concordia/migrations/0001_initial.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: concordia/migrations/0001_squashed_0040_remove_campaign_is_active.py
  function create_groups (line 10) | def create_groups(apps, schema_editor):
  class Migration (line 16) | class Migration(migrations.Migration):

FILE: concordia/migrations/0002_auto_20181004_1848.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0003_auto_20181004_2103.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: concordia/migrations/0004_auto_20181010_1715.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: concordia/migrations/0005_campaign_short_description.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0006_campaignresource.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: concordia/migrations/0007_thumbnail_images.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0008_auto_20181015_1711.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0009_project_description.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0010_auto_20181021_1659.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0010_auto_20181022_1530.py
  function handle_items_without_projects (line 6) | def handle_items_without_projects(apps, schema_editor):
  class Migration (line 11) | class Migration(migrations.Migration):

FILE: concordia/migrations/0011_auto_20181022_1532.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: concordia/migrations/0012_merge_20181022_1554.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0013_auto_20181031_1305.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0014_auto_20181115_1411.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0015_auto_20181115_1436.py
  function split_edit_statuses (line 8) | def split_edit_statuses(apps, schema_editor):
  class Migration (line 20) | class Migration(migrations.Migration):

FILE: concordia/migrations/0016_auto_20181115_1803.py
  function update_new_statuses (line 8) | def update_new_statuses(apps, schema_editor):
  class Migration (line 19) | class Migration(migrations.Migration):

FILE: concordia/migrations/0017_change_transcription_supersedes_related_name.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: concordia/migrations/0018_auto_20181128_1611.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0018_simplepage.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: concordia/migrations/0019_merge_20181128_1715.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0020_auto_20181128_1718.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0021_sitereport.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: concordia/migrations/0022_auto_20181211_1310.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0023_auto_20190130_1555.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0024_add_site_report_ordering.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0024_auto_20190211_1420.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0025_auto_20190329_1705.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0025_unicode_slugs.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0026_update_published_field_definition.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0027_merge_20190423_1657.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0028_asset_year.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0029_assettranscriptionreservation_reservation_token.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0030_auto_20190503_1559.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0031_auto_20190509_1142.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: concordia/migrations/0032_topic_ordering.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0033_simple_content_blocks.py
  function load_legacy_content_blocks (line 6) | def load_legacy_content_blocks(apps, schema_editor):
  class Migration (line 22) | class Migration(migrations.Migration):

FILE: concordia/migrations/0034_auto_20190627_1438.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0035_auto_20190627_1455.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0036_auto_20190703_1203.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0037_carouselslide.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0038_sitereport_topic.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: concordia/migrations/0039_auto_20200129_1536.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: concordia/migrations/0040_auto_20200130_1756.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: concordia/migrations/0041_auto_20200203_1351.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0042_auto_20200316_1623.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0043_auto_20200323_1729.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0044_auto_20200323_1827.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0045_auto_20200323_1832.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0046_auto_20200323_1907.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0047_auto_20200324_1103.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0048_auto_20200324_1820.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0049_auto_20200324_2004.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0050_auto_20210920_1544.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: concordia/migrations/0051_asset_storage_image.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: concordia/migrations/0052_auto_20220531_1331.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0053_banner.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0054_banner_active.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0055_campaign_status.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0056_auto_20220922_1508.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0057_resource_resource_type.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0058_banner_slug.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0059_resourcefile.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0060_alter_resourcefile_resource.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0061_auto_20230201_1453.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: concordia/migrations/0061_sitereport_registered_contributors.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0062_resourcefile_updated_on.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0062_userretiredcampaign.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: concordia/migrations/0063_banner_alert_status.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0064_alter_banner_alert_status.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0065_alter_userretiredcampaign_unique_together.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: concordia/migrations/0066_auto_20230217_1302.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: concordia/migrations/0066_campaignretirementprogress.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: concordia/migrations/0067_alter_campaignretirementprogress_campaign.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: concordia/migrations/0068_campaignretirementprogress_complete.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0069_merge_20230224_1446.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0070_alter_campaign_options.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0071_auto_20230306_1456.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: concordia/migrations/0072_merge_20230313_1047.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0073_auto_20230314_1327.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: concordia/migrations/0074_auto_20230314_1341.py
  function forwards_func (line 6) | def forwards_func(apps, schema_editor):
  function reverse_func (line 12) | def reverse_func(apps, schema_editor):
  class Migration (line 19) | class Migration(migrations.Migration):

FILE: concordia/migrations/0075_auto_20230327_1333.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0076_sitereport_report_name.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0077_alter_sitereport_report_name.py
  function update_report_names (line 6) | def update_report_names(apps, schema_editor):
  function backwards (line 26) | def backwards(apps, schema_editor):
  class Migration (line 31) | class Migration(migrations.Migration):

FILE: concordia/migrations/0078_alter_sitereport_report_name.py
  function update_report_names (line 6) | def update_report_names(apps, schema_editor):
  function backwards (line 16) | def backwards(apps, schema_editor):
  class Migration (line 28) | class Migration(migrations.Migration):

FILE: concordia/migrations/0079_auto_20230601_1234.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0080_auto_20230602_0920.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0081_sitereport_review_actions.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0082_delete_userretiredcampaign.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0083_sitereport_daily_active_users.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0084_rename_review_actions_sitereport_daily_review_actions.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0085_auto_20231016_1432.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: concordia/migrations/0086_auto_20231215_1311.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: concordia/migrations/0087_auto_20240213_0756.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: concordia/migrations/0088_alter_simplepage_body.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0089_campaign_image_alt_text.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0090_auto_20240408_1334.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0091_guide_simple_page.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: concordia/migrations/0092_auto_20240509_1522.py
  function set_simplepages (line 6) | def set_simplepages(apps, schema_editor):
  function backwards (line 15) | def backwards(apps, schema_editor):
  class Migration (line 22) | class Migration(migrations.Migration):

FILE: concordia/migrations/0093_asset_campaign.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: concordia/migrations/0094_alter_asset_campaign.py
  function set_field_values (line 7) | def set_field_values(apps, schema_editor):
  function revert_field_values (line 33) | def revert_field_values(apps, schema_editor):
  class Migration (line 40) | class Migration(migrations.Migration):

FILE: concordia/migrations/0095_transcription_rolled_back_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0096_transcription_source.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: concordia/migrations/0097_alter_sitereport_options_userprofile_review_count_and_more.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: concordia/migrations/0098_userprofile_create_and_population.py
  function create_and_populate_profiles (line 7) | def create_and_populate_profiles(apps, schema_editor):
  function revert_create_and_populate_profiles (line 19) | def revert_create_and_populate_profiles(apps, schema_editor):
  class Migration (line 26) | class Migration(migrations.Migration):

FILE: concordia/migrations/0099_alter_campaign_display_on_homepage_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0100_researchcenter.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0101_auto_20241119_1215.py
  function forwards_func (line 14) | def forwards_func(apps, schema_editor):
  function reverse_func (line 29) | def reverse_func(apps, schema_editor):
  class Migration (line 36) | class Migration(migrations.Migration):

FILE: concordia/migrations/0102_campaign_research_centers.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0103_alter_item_title.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0104_nexttranscribabletopicasset_and_more.py
  class Migration (line 10) | class Migration(migrations.Migration):

FILE: concordia/migrations/0105_nextreviewablecampaignasset_concordia_n_transcr_aafdba_gin_and_more.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: concordia/migrations/0106_alter_nextreviewablecampaignasset_options_and_more.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: concordia/migrations/0107_alter_nextreviewablecampaignasset_options_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0108_add_next_asset_cache_periodic_task.py
  function add_renew_next_asset_cache_task (line 6) | def add_renew_next_asset_cache_task(apps, schema_editor):
  function remove_renew_next_asset_cache_task (line 25) | def remove_renew_next_asset_cache_task(apps, schema_editor):
  class Migration (line 30) | class Migration(migrations.Migration):

FILE: concordia/migrations/0109_alter_nextreviewablecampaignasset_asset_and_more.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: concordia/migrations/0110_remove_asset_media_url_alter_asset_storage_image.py
  class Migration (line 9) | class Migration(migrations.Migration):

FILE: concordia/migrations/0111_auto_20250428_1023.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0112_projecttopic_url_filter_alter_projecttopic_id.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0113_create_asset_status_periodic_task.py
  function create_asset_status_task (line 6) | def create_asset_status_task(apps, schema_editor):
  function delete_asset_status_task (line 28) | def delete_asset_status_task(apps, schema_editor):
  class Migration (line 36) | class Migration(migrations.Migration):

FILE: concordia/migrations/0114_create_daily_activity_periodic_task.py
  function create_daily_activity_task (line 6) | def create_daily_activity_task(apps, schema_editor):
  function delete_daily_activity_task (line 32) | def delete_daily_activity_task(apps, schema_editor):
  class Migration (line 39) | class Migration(migrations.Migration):

FILE: concordia/migrations/0115_alter_asset_storage_image_alter_banner_link_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0116_item_thumbnail_image.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0117_alter_projecttopic_options_projecttopic_ordering.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0118_asset_concordia_a_item_id_f10916_idx_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0119_remove_asset_concordia_a_id_137ca8_idx_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0120_sitereport_assets_started.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0121_keymetricsreport.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0122_alter_item_title.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0123_alter_campaignretirementprogress_options.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0124_update_periodic_task_paths.py
  function forwards (line 45) | def forwards(apps, schema_editor):
  function backwards (line 53) | def backwards(apps, schema_editor):
  class Migration (line 61) | class Migration(migrations.Migration):

FILE: concordia/migrations/0125_update_userprofile_tasks.py
  function forwards (line 8) | def forwards(apps, schema_editor):
  function reverse_func (line 14) | def reverse_func(apps, schema_editor):
  class Migration (line 20) | class Migration(migrations.Migration):

FILE: concordia/migrations/0126_concordiafile_helpfullink_remove_resource_campaign_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/migrations/0127_alter_campaignretirementprogress_options_and_more.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: concordia/migrations/0128_alter_campaignretirementprogress_options.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: concordia/models.py
  function resource_file_upload_path (line 63) | def resource_file_upload_path(instance, filename):
  class ConcordiaUser (line 78) | class ConcordiaUser(User):
    class Meta (line 87) | class Meta:
    method email_reconfirmation_cache_key (line 91) | def email_reconfirmation_cache_key(self):
    method set_email_for_reconfirmation (line 97) | def set_email_for_reconfirmation(self, email):
    method get_email_for_reconfirmation (line 110) | def get_email_for_reconfirmation(self):
    method delete_email_for_reconfirmation (line 120) | def delete_email_for_reconfirmation(self):
    method get_email_reconfirmation_key (line 126) | def get_email_reconfirmation_key(self):
    method validate_reconfirmation_email (line 145) | def validate_reconfirmation_email(self, email):
    method review_incidents (line 157) | def review_incidents(self, recent_accepts, threshold=THRESHOLD):
    method transcribe_incidents (line 192) | def transcribe_incidents(self, transcriptions):
    method transcription_accepted_cache_key (line 222) | def transcription_accepted_cache_key(self):
    method check_and_track_accept_limit (line 228) | def check_and_track_accept_limit(self, transcription):
  class UserProfile (line 265) | class UserProfile(MetricsModelMixin("userprofile"), models.Model):
  class OverlayPosition (line 275) | class OverlayPosition(object):
  class TranscriptionStatus (line 287) | class TranscriptionStatus(object):
  class MediaType (line 312) | class MediaType:
  class PublicationQuerySet (line 324) | class PublicationQuerySet(models.QuerySet):
    method published (line 325) | def published(self):
    method unpublished (line 331) | def unpublished(self):
  class UnlistedPublicationQuerySet (line 338) | class UnlistedPublicationQuerySet(PublicationQuerySet):
    method annotated (line 339) | def annotated(self):
    method listed (line 418) | def listed(self):
    method unlisted (line 421) | def unlisted(self):
    method active (line 424) | def active(self):
    method completed (line 427) | def completed(self):
    method retired (line 430) | def retired(self):
    method get_next_transcription_campaigns (line 433) | def get_next_transcription_campaigns(self):
    method get_next_review_campaigns (line 436) | def get_next_review_campaigns(self):
  class Card (line 440) | class Card(models.Model):
    method __str__ (line 449) | def __str__(self):
    class Meta (line 452) | class Meta:
  class CardFamily (line 456) | class CardFamily(models.Model):
    class Meta (line 461) | class Meta:
    method __str__ (line 464) | def __str__(self):
  function on_cardfamily_save (line 468) | def on_cardfamily_save(sender, instance, **kwargs):
  class ResearchCenter (line 481) | class ResearchCenter(models.Model):
    method __str__ (line 484) | def __str__(self):
  class Campaign (line 488) | class Campaign(MetricsModelMixin("campaign"), models.Model):
    class Status (line 489) | class Status(models.IntegerChoices):
    class Meta (line 536) | class Meta:
    method __str__ (line 544) | def __str__(self):
    method get_absolute_url (line 547) | def get_absolute_url(self):
  class Topic (line 551) | class Topic(models.Model):
    class Meta (line 568) | class Meta:
    method __str__ (line 573) | def __str__(self):
    method get_absolute_url (line 576) | def get_absolute_url(self):
  class HelpfulLinkTypeQuerySet (line 580) | class HelpfulLinkTypeQuerySet(models.QuerySet):
    method related_links (line 581) | def related_links(self):
    method completed_transcription_links (line 584) | def completed_transcription_links(self):
  class HelpfulLink (line 590) | class HelpfulLink(MetricsModelMixin("resource"), models.Model):
    class HelpfulLinkType (line 598) | class HelpfulLinkType(models.IntegerChoices):
    class Meta (line 618) | class Meta:
    method __str__ (line 622) | def __str__(self):
  class ConcordiaFile (line 626) | class ConcordiaFile(models.Model):
    class Meta (line 641) | class Meta:
    method __str__ (line 645) | def __str__(self):
    method save (line 648) | def save(self, *args, **kwargs):
    method delete (line 654) | def delete(self, *args, **kwargs):
  class Project (line 663) | class Project(MetricsModelMixin("project"), models.Model):
    class Meta (line 687) | class Meta:
    method __str__ (line 695) | def __str__(self):
    method get_absolute_url (line 698) | def get_absolute_url(self):
    method turn_off_ocr (line 704) | def turn_off_ocr(self):
  class Item (line 708) | class Item(MetricsModelMixin("item"), models.Model):
    class Meta (line 736) | class Meta:
    method __str__ (line 740) | def __str__(self):
    method get_absolute_url (line 743) | def get_absolute_url(self):
    method thumbnail_link (line 754) | def thumbnail_link(self) -> str | None:
    method turn_off_ocr (line 773) | def turn_off_ocr(self):
  class AssetQuerySet (line 777) | class AssetQuerySet(PublicationQuerySet):
    method add_contribution_counts (line 778) | def add_contribution_counts(self):
  class Asset (line 788) | class Asset(MetricsModelMixin("asset"), models.Model):
    method get_storage_path (line 789) | def get_storage_path(self, filename):
    class Meta (line 839) | class Meta:
    method __str__ (line 856) | def __str__(self):
    method save (line 859) | def save(self, *args, **kwargs):
    method get_absolute_url (line 870) | def get_absolute_url(self):
    method logger (line 882) | def logger(self):
    method latest_transcription (line 885) | def latest_transcription(self):
    method get_asset_image_path (line 889) | def get_asset_image_path(item):
    method get_asset_image_filename (line 892) | def get_asset_image_filename(self, extension="jpg"):
    method get_existing_storage_image_filename (line 897) | def get_existing_storage_image_filename(self):
    method get_ocr_transcript (line 900) | def get_ocr_transcript(self, language=None):
    method get_contributor_count (line 927) | def get_contributor_count(self):
    method turn_off_ocr (line 938) | def turn_off_ocr(self):
    method can_rollback (line 941) | def can_rollback(
    method rollback_transcription (line 1048) | def rollback_transcription(self, user: User) -> "Transcription":
    method can_rollforward (line 1118) | def can_rollforward(
    method rollforward_transcription (line 1318) | def rollforward_transcription(self, user: User) -> "Transcription":
  class Tag (line 1396) | class Tag(MetricsModelMixin("tag"), models.Model):
    method __str__ (line 1400) | def __str__(self):
  class UserAssetTagCollection (line 1404) | class UserAssetTagCollection(
    method __str__ (line 1415) | def __str__(self):
  class TranscriptionManager (line 1419) | class TranscriptionManager(models.Manager):
    method review_actions (line 1420) | def review_actions(self, start, end=None):
    method recent_review_actions (line 1428) | def recent_review_actions(self, days=1):
    method review_incidents (line 1432) | def review_incidents(self, start=ONE_DAY_AGO):
    method recent_transcriptions (line 1456) | def recent_transcriptions(self, start=ONE_DAY_AGO):
    method transcribe_incidents (line 1461) | def transcribe_incidents(self, start=ONE_DAY_AGO):
  class Transcription (line 1487) | class Transcription(MetricsModelMixin("transcription"), models.Model):
    class Meta (line 1552) | class Meta:
    method __str__ (line 1557) | def __str__(self):
    method campaign_slug (line 1560) | def campaign_slug(self):
    method clean (line 1563) | def clean(self):
    method status (line 1576) | def status(self):
  function update_userprofileactivity_table (line 1585) | def update_userprofileactivity_table(user, campaign_id, field, increment...
  function _update_useractivity_cache (line 1664) | def _update_useractivity_cache(user_id, campaign_id, attr_name):
  class AssetTranscriptionReservation (line 1698) | class AssetTranscriptionReservation(models.Model):
    method get_token (line 1714) | def get_token(self):
    method get_user (line 1717) | def get_user(self):
  class SimplePage (line 1721) | class SimplePage(models.Model):
    method __str__ (line 1742) | def __str__(self):
  class Banner (line 1746) | class Banner(models.Model):
    method __str__ (line 1779) | def __str__(self):
    method alert_class (line 1782) | def alert_class(self):
    method btn_class (line 1785) | def btn_class(self):
  class CarouselSlide (line 1789) | class CarouselSlide(models.Model):
    method __str__ (line 1819) | def __str__(self):
  class SiteReportManager (line 1823) | class SiteReportManager(models.Manager):
    method _series_filter (line 1838) | def _series_filter(
    method previous_in_series (line 1872) | def previous_in_series(
    method series_filter_for_instance (line 1899) | def series_filter_for_instance(self, instance: "SiteReport") -> Q:
    method previous_for_instance (line 1919) | def previous_for_instance(self, instance: "SiteReport") -> "SiteReport...
    method next_for_instance (line 1930) | def next_for_instance(self, instance: "SiteReport") -> "SiteReport | N...
    method last_on_or_before_date_for_series (line 1941) | def last_on_or_before_date_for_series(
    method first_on_or_after_date_for_series (line 1960) | def first_on_or_after_date_for_series(
    method sum_assets_started_for_series_between_dates (line 1980) | def sum_assets_started_for_series_between_dates(
  class SiteReport (line 2002) | class SiteReport(models.Model):
    class ReportName (line 2003) | class ReportName(models.TextChoices):
    class Meta (line 2041) | class Meta:
    method calculate_assets_started (line 2078) | def calculate_assets_started(
    method previous_in_series (line 2120) | def previous_in_series(self) -> "SiteReport | None":
    method next_in_series (line 2126) | def next_in_series(self) -> "SiteReport | None":
    method to_debug_dict (line 2132) | def to_debug_dict(self) -> dict:
    method to_debug_json (line 2192) | def to_debug_json(self) -> str:
  class KeyMetricsReport (line 2202) | class KeyMetricsReport(models.Model):
    class PeriodType (line 2240) | class PeriodType(models.TextChoices):
    class Meta (line 2276) | class Meta:
    method __str__ (line 2325) | def __str__(self) -> str:
    method get_fiscal_year_for_date (line 2358) | def get_fiscal_year_for_date(d: datetime.date) -> int:
    method get_fiscal_quarter_for_date (line 2363) | def get_fiscal_quarter_for_date(d: datetime.date) -> int:
    method month_bounds (line 2374) | def month_bounds(d: datetime.date) -> tuple[datetime.date, datetime.da...
    method _monthly_from_sitereports (line 2385) | def _monthly_from_sitereports(
    method upsert_month (line 2495) | def upsert_month(cls, *, year: int, month: int) -> Optional["KeyMetric...
    method upsert_quarter (line 2532) | def upsert_quarter(
    method upsert_fiscal_year (line 2639) | def upsert_fiscal_year(cls, *, fiscal_year: int) -> Optional["KeyMetri...
    method csv_filename (line 2717) | def csv_filename(self) -> str:
    method _format_value_for_csv (line 2737) | def _format_value_for_csv(self, field_name: str, value) -> str | int |...
    method _calendar_year_for_month_in_fy (line 2761) | def _calendar_year_for_month_in_fy(self, month: int, fiscal_year: int)...
    method _fy_abbrev (line 2767) | def _fy_abbrev(self, fiscal_year: int) -> str:
    method _month_label (line 2776) | def _month_label(self, fiscal_year: int, month: int) -> str:
    method _format_cell (line 2782) | def _format_cell(self, field_name: str, value):
    method _csv_matrix_monthly (line 2788) | def _csv_matrix_monthly(self) -> tuple[list[str], list[list[str | int ...
    method _quarter_month_specs (line 2804) | def _quarter_month_specs(self) -> list[tuple[int, int]]:
    method _csv_matrix_quarterly (line 2820) | def _csv_matrix_quarterly(self) -> tuple[list[str], list[list[str | in...
    method _csv_matrix_fiscal_year (line 2940) | def _csv_matrix_fiscal_year(
    method render_csv (line 3051) | def render_csv(self) -> bytes:
  class UserProfileActivity (line 3080) | class UserProfileActivity(models.Model):
    class Meta (line 3101) | class Meta:
    method __str__ (line 3109) | def __str__(self):
    method get_status (line 3112) | def get_status(self):
    method total_actions (line 3116) | def total_actions(self):
  class CampaignRetirementProgress (line 3122) | class CampaignRetirementProgress(models.Model):
    method __str__ (line 3142) | def __str__(self):
    class Meta (line 3145) | class Meta:
  class TutorialCard (line 3149) | class TutorialCard(models.Model):
    class Meta (line 3158) | class Meta:
  class Guide (line 3162) | class Guide(models.Model):
    method __str__ (line 3179) | def __str__(self):
  function validated_get_or_create (line 3183) | def validated_get_or_create(klass, **kwargs):
  class NextAsset (line 3219) | class NextAsset(models.Model):
    class Meta (line 3238) | class Meta:
    method __str__ (line 3241) | def __str__(self):
  class NextTranscribableAsset (line 3245) | class NextTranscribableAsset(NextAsset):
    class Meta (line 3258) | class Meta:
  class NextReviewableAsset (line 3262) | class NextReviewableAsset(NextAsset):
    class Meta (line 3276) | class Meta:
  class NextCampaignAssetManager (line 3280) | class NextCampaignAssetManager(models.Manager):
    method needed_for_campaign (line 3290) | def needed_for_campaign(self, campaign_id, target_count=None):
  class NextTopicAssetManager (line 3314) | class NextTopicAssetManager(models.Manager):
    method needed_for_topic (line 3324) | def needed_for_topic(self, topic_id, target_count=None):
  class NextTranscribableCampaignAssetManager (line 3348) | class NextTranscribableCampaignAssetManager(NextCampaignAssetManager):
  class NextTranscribableTopicAssetManager (line 3352) | class NextTranscribableTopicAssetManager(NextTopicAssetManager):
  class NextReviewableCampaignAssetManager (line 3356) | class NextReviewableCampaignAssetManager(NextCampaignAssetManager):
  class NextReviewableTopicAssetManager (line 3360) | class NextReviewableTopicAssetManager(NextTopicAssetManager):
  class NextTranscribableCampaignAsset (line 3364) | class NextTranscribableCampaignAsset(NextTranscribableAsset):
    class Meta (line 3374) | class Meta:
  class NextTranscribableTopicAsset (line 3379) | class NextTranscribableTopicAsset(NextTranscribableAsset):
    class Meta (line 3389) | class Meta:
  class NextReviewableCampaignAsset (line 3395) | class NextReviewableCampaignAsset(NextReviewableAsset):
    class Meta (line 3405) | class Meta:
  class NextReviewableTopicAsset (line 3413) | class NextReviewableTopicAsset(NextReviewableAsset):
    class Meta (line 3423) | class Meta:
  class ProjectTopic (line 3432) | class ProjectTopic(models.Model):
    class Meta (line 3454) | class Meta:

FILE: concordia/parser.py
  class OGImageParser (line 13) | class OGImageParser(HTMLParser):
    method __init__ (line 14) | def __init__(self):
    method handle_starttag (line 18) | def handle_starttag(self, tag, attrs):
  function extract_og_image (line 26) | def extract_og_image(url):
  function get_og_image (line 48) | def get_og_image(url):
  function fetch_blog_posts (line 58) | def fetch_blog_posts():
  function paginate_blog_posts (line 103) | def paginate_blog_posts():

FILE: concordia/passwords/validators.py
  class ComplexityValidator (line 56) | class ComplexityValidator(object):
    method __init__ (line 78) | def __init__(self, complexities: Mapping[str, int] | None):
    method __call__ (line 89) | def __call__(self, value: str) -> None:

FILE: concordia/secrets.py
  function get_secret (line 9) | def get_secret(secret_name):

FILE: concordia/settings_docker.py
  function whitenoise_immutable_file_test (line 13) | def whitenoise_immutable_file_test(static_url):

FILE: concordia/signals/handlers.py
  function clear_reservation_token (line 42) | def clear_reservation_token(
  function handle_user_login_failed (line 87) | def handle_user_login_failed(
  function user_successfully_activated (line 109) | def user_successfully_activated(
  function add_user_to_newsletter (line 166) | def add_user_to_newsletter(
  function update_asset_status (line 197) | def update_asset_status(
  function send_asset_update (line 281) | def send_asset_update(
  function send_asset_reservation_obtained (line 322) | def send_asset_reservation_obtained(sender: Any, **kwargs: Any) -> None:
  function send_asset_reservation_released (line 357) | def send_asset_reservation_released(sender: Any, **kwargs: Any) -> None:
  function send_asset_reservation_message (line 389) | def send_asset_reservation_message(
  function remove_file_from_s3 (line 428) | def remove_file_from_s3(
  function create_user_profile (line 450) | def create_user_profile(
  function on_transcription_save (line 476) | def on_transcription_save(
  function add_request_id_to_response (line 526) | def add_request_id_to_response(

FILE: concordia/static/admin/custom-inline.js
  function triggerChangeOnField (line 4) | function triggerChangeOnField(win, chosenId) {

FILE: concordia/static/admin/editor-preview.js
  function updatePreview (line 53) | function updatePreview() {
  function queueUpdate (line 83) | function queueUpdate() {

FILE: concordia/static/js/src/asset-reservation.js
  function attemptToReserveAsset (line 13) | function attemptToReserveAsset(reservationURL, findANewPageURL, actionTy...
  function reserveAssetForEditing (line 126) | function reserveAssetForEditing() {

FILE: concordia/static/js/src/base.js
  function csrfSafeMethod (line 20) | function csrfSafeMethod(method) {
  function buildErrorMessage (line 44) | function buildErrorMessage(jqXHR, textStatus, errorThrown) {
  function displayHtmlMessage (line 56) | function displayHtmlMessage(level, message, uniqueId) {
  function displayMessage (line 100) | function displayMessage(level, message, uniqueId) {
  function isOutdatedBrowser (line 108) | function isOutdatedBrowser() {
  function loadLegacyPolyfill (line 117) | function loadLegacyPolyfill(scriptUrl, callback) {
  function appendAccountItem (line 190) | function appendAccountItem(link, $menu) {
  function debounce (line 274) | function debounce(function_, timeout = 300) {
  function trackShareInteraction (line 298) | function trackShareInteraction($element, interactionType) {
  function trackUIInteraction (line 383) | function trackUIInteraction(element, category, action, label) {

FILE: concordia/static/js/src/contribute.js
  function lockControls (line 8) | function lockControls($container) {
  function unlockControls (line 18) | function unlockControls($container) {
  function resetTurnstile (line 70) | function resetTurnstile() {
  function setupPage (line 76) | function setupPage() {

FILE: concordia/static/js/src/filter-assets.js
  function filterAssets (line 1) | function filterAssets(doFilter, url) {

FILE: concordia/static/js/src/guide.js
  function openOffcanvas (line 7) | function openOffcanvas() {
  function closeOffcanvas (line 25) | function closeOffcanvas() {
  function trackHowToInteraction (line 78) | function trackHowToInteraction(element, label) {

FILE: concordia/static/js/src/modules/accessible-colors.js
  function adjustColorForContrast (line 10) | function adjustColorForContrast(
  function generateAccessibleColors (line 47) | function generateAccessibleColors(

FILE: concordia/static/js/src/modules/concordia-visualization.js
  class ConcordiaVisualization (line 13) | class ConcordiaVisualization {
    method constructor (line 42) | constructor({
    method render (line 90) | async render() {
    method _handleError (line 318) | _handleError(context, message) {
    method _defaultOptions (line 342) | static _defaultOptions(title, xLabel, yLabel) {
    method _deepMerge (line 389) | static _deepMerge(target, ...sources) {

FILE: concordia/static/js/src/modules/quick-tips.js
  function clearCache (line 4) | function clearCache() {
  function initCampaignTutorial (line 13) | function initCampaignTutorial() {
  function setTutorialHeight (line 54) | function setTutorialHeight() {

FILE: concordia/static/js/src/modules/turnstile.js
  function resetTurnstile (line 3) | function resetTurnstile(widgetId) {

FILE: concordia/static/js/src/modules/visualization-errors.js
  function renderEmptyChart (line 14) | function renderEmptyChart(
  function renderErrorOverlay (line 76) | function renderErrorOverlay(

FILE: concordia/static/js/src/ocr.js
  function selectLanguage (line 33) | function selectLanguage() {

FILE: concordia/static/js/src/quick-tips-setup.js
  function trackQuickTipsInteraction (line 10) | function trackQuickTipsInteraction(element, label) {

FILE: concordia/static/js/src/recent-pages.js
  function getPages (line 5) | function getPages(queryString = window.location.search) {
  function finalizePageUpdate (line 131) | function finalizePageUpdate(currentParameters) {

FILE: concordia/static/js/src/viewer-split.js
  function saveSizes (line 37) | function saveSizes(sizes) {
  function saveDirection (line 49) | function saveDirection(direction) {
  function verticalSplit (line 56) | function verticalSplit() {
  function horizontalSplit (line 86) | function horizontalSplit() {

FILE: concordia/static/js/src/viewer.js
  function updateFilters (line 146) | function updateFilters() {
  function stepUp (line 200) | function stepUp(id) {
  function stepDown (line 208) | function stepDown(id) {
  function resetImageFilterForms (line 216) | function resetImageFilterForms() {

FILE: concordia/static/vendor/jquery.cookie.js
  function encode (line 23) | function encode(s) {
  function decode (line 27) | function decode(s) {
  function stringifyCookieValue (line 31) | function stringifyCookieValue(value) {
  function parseCookieValue (line 35) | function parseCookieValue(s) {
  function read (line 50) | function read(s, converter) {

FILE: concordia/storage.py
  class LazyAssetStorage (line 5) | class LazyAssetStorage(LazyObject):
    method _setup (line 6) | def _setup(self):
  class LazyVisualizationStorage (line 10) | class LazyVisualizationStorage(LazyObject):
    method _setup (line 11) | def _setup(self):

FILE: concordia/storage_backends.py
  class OverwriteS3Boto3Storage (line 4) | class OverwriteS3Boto3Storage(S3Boto3Storage):
    method get_available_name (line 5) | def get_available_name(self, name, max_length=None):

FILE: concordia/tasks/assets.py
  function calculate_difficulty_values (line 19) | def calculate_difficulty_values(asset_qs=None):
  function populate_asset_years (line 68) | def populate_asset_years():
  function fix_storage_images (line 109) | def fix_storage_images(campaign_slug=None, asset_start_id=None):

FILE: concordia/tasks/blog.py
  function fetch_and_cache_blog_images (line 13) | def fetch_and_cache_blog_images(self):

FILE: concordia/tasks/housekeeping.py
  function clear_sessions (line 14) | def clear_sessions():

FILE: concordia/tasks/next_asset/renew.py
  function renew_next_asset_cache (line 23) | def renew_next_asset_cache(self):

FILE: concordia/tasks/next_asset/reviewable.py
  function populate_next_reviewable_for_campaign (line 28) | def populate_next_reviewable_for_campaign(self, campaign_id):
  function populate_next_reviewable_for_topic (line 124) | def populate_next_reviewable_for_topic(self, topic_id):
  function clean_next_reviewable_for_campaign (line 215) | def clean_next_reviewable_for_campaign(self, campaign_id):
  function clean_next_reviewable_for_topic (line 241) | def clean_next_reviewable_for_topic(self, topic_id):

FILE: concordia/tasks/next_asset/transcribable.py
  function populate_next_transcribable_for_campaign (line 26) | def populate_next_transcribable_for_campaign(self, campaign_id):
  function populate_next_transcribable_for_topic (line 95) | def populate_next_transcribable_for_topic(self, topic_id):
  function clean_next_transcribable_for_campaign (line 159) | def clean_next_transcribable_for_campaign(self, campaign_id):
  function clean_next_transcribable_for_topic (line 186) | def clean_next_transcribable_for_topic(self, topic_id):

FILE: concordia/tasks/reports/backfill.py
  function backfill_assets_started_for_site_reports (line 30) | def backfill_assets_started_for_site_reports(self, skip_existing: bool =...

FILE: concordia/tasks/reports/key_metrics.py
  function build_key_metrics_reports (line 19) | def build_key_metrics_reports(self, recompute_all: bool = False) -> int:

FILE: concordia/tasks/reports/sitereport.py
  function _recent_transcriptions (line 28) | def _recent_transcriptions() -> QuerySet[Transcription]:
  function _daily_active_users (line 55) | def _daily_active_users() -> int:
  function site_report (line 87) | def site_report() -> None:
  function topic_report (line 270) | def topic_report(topic: Topic) -> None:
  function campaign_report (line 406) | def campaign_report(campaign: Campaign) -> SiteReport:
  function retired_total_report (line 578) | def retired_total_report() -> None:

FILE: concordia/tasks/reservations.py
  function expire_inactive_asset_reservations (line 18) | def expire_inactive_asset_reservations():
  function tombstone_old_active_asset_reservations (line 56) | def tombstone_old_active_asset_reservations():
  function delete_old_tombstoned_reservations (line 85) | def delete_old_tombstoned_reservations():

FILE: concordia/tasks/retirement.py
  function retire_campaign (line 18) | def retire_campaign(campaign_id):
  function project_removal_success (line 70) | def project_removal_success(project_id, campaign_id):
  function remove_next_project (line 103) | def remove_next_project(campaign_id):
  function item_removal_success (line 137) | def item_removal_success(item_id, campaign_id, project_id):
  function remove_next_item (line 171) | def remove_next_item(project_id):
  function assets_removal_success (line 200) | def assets_removal_success(asset_ids, campaign_id, item_id):
  function remove_next_assets (line 235) | def remove_next_assets(item_id):
  function delete_asset (line 267) | def delete_asset(asset_id):

FILE: concordia/tasks/search_index.py
  function create_opensearch_indices (line 14) | def create_opensearch_indices():
  function delete_opensearch_indices (line 27) | def delete_opensearch_indices():
  function rebuild_opensearch_indices (line 38) | def rebuild_opensearch_indices():
  function populate_opensearch_users_indices (line 51) | def populate_opensearch_users_indices():
  function populate_opensearch_assets_indices (line 66) | def populate_opensearch_assets_indices():
  function populate_opensearch_indices (line 87) | def populate_opensearch_indices():

FILE: concordia/tasks/thumbnails.py
  function download_item_thumbnail_task (line 26) | def download_item_thumbnail_task(
  function download_missing_thumbnails_task (line 69) | def download_missing_thumbnails_task(

FILE: concordia/tasks/unusualactivity.py
  function unusual_activity (line 22) | def unusual_activity(ignore_env: bool = False) -> None:

FILE: concordia/tasks/useractivity.py
  function _populate_activity_table (line 31) | def _populate_activity_table(campaigns: Iterable[Campaign]) -> None:
  function populate_completed_campaign_counts (line 114) | def populate_completed_campaign_counts() -> None:
  function populate_active_campaign_counts (line 132) | def populate_active_campaign_counts() -> None:
  function update_useractivity_cache (line 149) | def update_useractivity_cache(
  function update_userprofileactivity_from_cache (line 248) | def update_userprofileactivity_from_cache(self) -> None:

FILE: concordia/tasks/visualizations.py
  function populate_asset_status_visualization_cache (line 24) | def populate_asset_status_visualization_cache(self) -> None:
  function populate_daily_activity_visualization_cache (line 152) | def populate_daily_activity_visualization_cache(self) -> None:

FILE: concordia/templatetags/concordia_filtering_tags.py
  function transcription_status_filters (line 12) | def transcription_status_filters(

FILE: concordia/templatetags/concordia_media_tags.py
  function asset_media_url (line 9) | def asset_media_url(asset: Any) -> str:

FILE: concordia/templatetags/concordia_querystring.py
  class QueryStringAlterer (line 17) | class QueryStringAlterer(Node):
    method __init__ (line 79) | def __init__(self, base_qs: str, as_variable: Optional[str], *args) ->...
    method render (line 85) | def render(self, context: Any) -> str:
    method qs_alter_tag (line 137) | def qs_alter_tag(cls, parser: Parser, token: Token) -> "QueryStringAlt...

FILE: concordia/templatetags/concordia_sharing_tags.py
  function share_buttons (line 9) | def share_buttons(url: str, title: str) -> Dict[str, str]:

FILE: concordia/templatetags/concordia_text_tags.py
  function normalize_whitespace (line 11) | def normalize_whitespace(text: str) -> str:
  function reprchar (line 39) | def reprchar(character: str) -> str:

FILE: concordia/templatetags/custom_math.py
  function multiply (line 9) | def multiply(value: Any, arg: Any) -> Any:

FILE: concordia/templatetags/group_list.py
  function batch (line 9) | def batch(value: Sequence[Any], size: int) -> list[Sequence[Any]]:

FILE: concordia/templatetags/reject_filter.py
  function reject (line 9) | def reject(value: Any, args: str) -> Any:

FILE: concordia/templatetags/truncation.py
  class WordBreakTruncator (line 10) | class WordBreakTruncator(Truncator):
    method word_break (line 11) | def word_break(self, num: int, truncate: str | None = None) -> str:
    method _text_word_break (line 44) | def _text_word_break(
  function truncatechars_on_word_break (line 83) | def truncatechars_on_word_break(value: str, arg: int | str) -> str:

FILE: concordia/templatetags/visualization.py
  function concordia_visualization (line 11) | def concordia_visualization(name: str, **attrs) -> SafeString:

FILE: concordia/tests/axe.py
  class Axe (line 18) | class Axe:
    method __init__ (line 19) | def __init__(self, py, script_path=_DEFAULT_SCRIPT):
    method violations (line 23) | def violations(self, report=None):
    method inject (line 46) | def inject(self):
    method run (line 53) | def run(self, context=None, options=None):
    method report (line 80) | def report(self, violations):
    method write_results (line 120) | def write_results(self, data, name=None):

FILE: concordia/tests/test_account_views.py
  class ConcordiaAccountViewTests (line 30) | class ConcordiaAccountViewTests(
    method setUp (line 37) | def setUp(self):
    method tearDown (line 40) | def tearDown(self):
    method test_AccountProfileView_get (line 43) | def test_AccountProfileView_get(self):
    method test_AccountProfileView_post (line 105) | def test_AccountProfileView_post(self):
    method test_AccountProfileView_post_invalid_form (line 168) | def test_AccountProfileView_post_invalid_form(self):
    method test_ajax_session_status_anon (line 184) | def test_ajax_session_status_anon(self):
    method test_ajax_session_status (line 190) | def test_ajax_session_status(self):
    method test_ajax_session_status_staff (line 204) | def test_ajax_session_status_staff(self):
    method test_ajax_messages (line 218) | def test_ajax_messages(self):
    method test_email_reconfirmation (line 230) | def test_email_reconfirmation(self):
    method test_account_letter (line 359) | def test_account_letter(self):
    method test_get_pages (line 369) | def test_get_pages(self):
    method test_AccountDeletionView (line 407) | def test_AccountDeletionView(self):

FILE: concordia/tests/test_admin.py
  class ConcordiaUserAdminTest (line 54) | class ConcordiaUserAdminTest(TestCase, CreateTestUsers, StreamingTestMix...
    method setUp (line 55) | def setUp(self):
    method test_transcription_count (line 63) | def test_transcription_count(self):
    method test_review_count (line 78) | def test_review_count(self):
    method test_csv_export (line 98) | def test_csv_export(self):
    method test_excel_export (line 119) | def test_excel_export(self):
  class CampaignAdminTest (line 129) | class CampaignAdminTest(TestCase, CreateTestUsers, StreamingTestMixin):
    method setUp (line 130) | def setUp(self):
    method test_truncated_description (line 141) | def test_truncated_description(self):
    method test_truncated_metadata (line 148) | def test_truncated_metadata(self):
    method test_retire (line 156) | def test_retire(self):
    method test_campaign_admin (line 194) | def test_campaign_admin(self):
  class HelpfulLinkAdminTest (line 204) | class HelpfulLinkAdminTest(TestCase, CreateTestUsers):
    method setUp (line 205) | def setUp(self):
    method test_helpfullink_admin (line 208) | def test_helpfullink_admin(self):
  class ConcordiaFileAdminTest (line 214) | class ConcordiaFileAdminTest(TestCase, CreateTestUsers):
    method setUp (line 215) | def setUp(self):
    method test_link_url (line 224) | def test_link_url(self):
    method test_get_fields (line 234) | def test_get_fields(self):
  class ProjectAdminTest (line 245) | class ProjectAdminTest(TestCase, CreateTestUsers):
    method setUp (line 246) | def setUp(self):
    method test_lookup_allowed (line 254) | def test_lookup_allowed(self):
    method test_item_import_view (line 259) | def test_item_import_view(self):
  class ItemAdminTest (line 309) | class ItemAdminTest(TestCase, CreateTestUsers):
    method setUp (line 310) | def setUp(self):
    method test_lookup_allowed (line 321) | def test_lookup_allowed(self):
    method test_get_deleted_objects (line 326) | def test_get_deleted_objects(self):
    method test_get_queryset (line 356) | def test_get_queryset(self):
    method test_campaign_title (line 361) | def test_campaign_title(self):
  class AssetAdminTest (line 367) | class AssetAdminTest(TestCase, CreateTestUsers):
    method setUp (line 368) | def setUp(self):
    method test_get_queryset (line 378) | def test_get_queryset(self):
    method test_lookup_allowed (line 383) | def test_lookup_allowed(self):
    method test_item_id (line 390) | def test_item_id(self):
    method test_truncated_storage_image (line 393) | def test_truncated_storage_image(self):
    method test_get_readonly_fields (line 404) | def test_get_readonly_fields(self):
    method test_change_view (line 409) | def test_change_view(self):
    method test_has_reopen_permission (line 419) | def test_has_reopen_permission(self):
    method test_response_action_redirects_with_valid_next (line 427) | def test_response_action_redirects_with_valid_next(self):
    method test_response_action_falls_back_to_default_without_valid_next (line 443) | def test_response_action_falls_back_to_default_without_valid_next(self):
    method test_change_view_skips_asset_logic_when_no_object_id (line 462) | def test_change_view_skips_asset_logic_when_no_object_id(self):
    method test_change_view_handles_submitted_status_as_needs_review (line 476) | def test_change_view_handles_submitted_status_as_needs_review(self):
    method test_response_action_returns_default_when_no_next_url (line 514) | def test_response_action_returns_default_when_no_next_url(self):
  class TagAdminTest (line 536) | class TagAdminTest(TestCase, CreateTestUsers, StreamingTestMixin):
    method setUp (line 537) | def setUp(self):
    method test_lookup_allowed (line 544) | def test_lookup_allowed(self):
    method test_export_tags_as_csv (line 553) | def test_export_tags_as_csv(self):
  class TranscriptionAdminTest (line 581) | class TranscriptionAdminTest(TestCase, CreateTestUsers, StreamingTestMix...
    method setUp (line 582) | def setUp(self):
    method test_lookup_allowed (line 596) | def test_lookup_allowed(self):
    method test_truncated_text (line 605) | def test_truncated_text(self):
    method test_export_to_csv (line 615) | def test_export_to_csv(self):
    method test_export_to_excel (line 639) | def test_export_to_excel(self):
    method test_show_full_result_count_is_disabled (line 646) | def test_show_full_result_count_is_disabled(self):
    method test_list_display_includes_superseded (line 649) | def test_list_display_includes_superseded(self):
    method test_list_filter_includes_superseded_param (line 652) | def test_list_filter_includes_superseded_param(self):
    method test_get_queryset_adds_is_superseded_annotation (line 660) | def test_get_queryset_adds_is_superseded_annotation(self):
    method test_superseded_column_uses_annotation_boolean (line 674) | def test_superseded_column_uses_annotation_boolean(self):
  class SiteReportAdminTest (line 686) | class SiteReportAdminTest(TestCase, CreateTestUsers, StreamingTestMixin):
    method setUp (line 687) | def setUp(self):
    method test_report_type (line 701) | def test_report_type(self):
    method test_export_to_csv (line 721) | def test_export_to_csv(self):
    method test_export_to_excel (line 744) | def test_export_to_excel(self):
    method test_report_type_variants (line 751) | def test_report_type_variants(self):
    method test_report_json_pretty_wrap (line 775) | def test_report_json_pretty_wrap(self):
    method test_previous_and_next_in_series_links (line 783) | def test_previous_and_next_in_series_links(self):
  class CampaignRetirementProgressAdminTest (line 827) | class CampaignRetirementProgressAdminTest(TestCase):
    method setUp (line 828) | def setUp(self):
    method test_completion (line 847) | def test_completion(self):
  class KeyMetricsReportAdminTests (line 865) | class KeyMetricsReportAdminTests(CreateTestUsers, TestCase):
    method setUp (line 866) | def setUp(self):
    method _make_monthly (line 872) | def _make_monthly(self):
    method test_download_csv_link_builds_expected_anchor (line 882) | def test_download_csv_link_builds_expected_anchor(self):
    method test_get_urls_registers_named_view (line 890) | def test_get_urls_registers_named_view(self):
    method test_download_csv_view_success (line 895) | def test_download_csv_view_success(self):
    method test_download_csv_view_404_when_missing (line 920) | def test_download_csv_view_404_when_missing(self):
    method test_download_selected_as_zip_streams_zip_with_csvs (line 929) | def test_download_selected_as_zip_streams_zip_with_csvs(self):

FILE: concordia/tests/test_admin_actions.py
  class MockModelAdmin (line 37) | class MockModelAdmin:
  class UserAdminActionTest (line 45) | class UserAdminActionTest(TestCase, CreateTestUsers):
    method setUp (line 46) | def setUp(self):
    method test_anonymize_action (line 51) | def test_anonymize_action(self):
  class ItemAdminActionTest (line 79) | class ItemAdminActionTest(TestCase):
    method _setUp (line 80) | def _setUp(self, published=True):
    method test_publish_item_action (line 98) | def test_publish_item_action(self):
    method test_unpublish_item_action (line 118) | def test_unpublish_item_action(self):
  class AssetAdminActionTest (line 139) | class AssetAdminActionTest(TestCase, CreateTestUsers):
    method setUp (line 140) | def setUp(self):
    method test_change_status_to_completed (line 165) | def test_change_status_to_completed(self):
    method test_change_status_to_needs_review (line 183) | def test_change_status_to_needs_review(self):
    method test_change_status_to_in_progress (line 201) | def test_change_status_to_in_progress(self):
    method test_change_status_to_completed_message_single (line 219) | def test_change_status_to_completed_message_single(self):
    method test_change_status_to_completed_message_multiple (line 231) | def test_change_status_to_completed_message_multiple(self):
    method test_change_status_to_needs_review_message_single (line 243) | def test_change_status_to_needs_review_message_single(self):
    method test_change_status_to_in_progress_message_multiple (line 255) | def test_change_status_to_in_progress_message_multiple(self):
  class AdminActionTest (line 273) | class AdminActionTest(TestCase):
    method _setUp (line 274) | def _setUp(self, published=True):
    method test_publish_action (line 309) | def test_publish_action(self):
    method test_unpublish_action (line 351) | def test_unpublish_action(self):
  class VerifyAssetsActionTest (line 394) | class VerifyAssetsActionTest(TestCase):
    method setUp (line 395) | def setUp(self):
    method test_verify_assets_action_for_campaign (line 421) | def test_verify_assets_action_for_campaign(self):
    method test_verify_assets_action_for_project (line 455) | def test_verify_assets_action_for_project(self):
    method test_verify_assets_action_for_item (line 472) | def test_verify_assets_action_for_item(self):
    method test_verify_assets_action_for_asset (line 489) | def test_verify_assets_action_for_asset(self):
    method test_verify_assets_action_for_unsupported_model (line 506) | def test_verify_assets_action_for_unsupported_model(self):

FILE: concordia/tests/test_admin_filters.py
  class NullableTimestampFilterTest (line 50) | class NullableTimestampFilterTest(CreateTestUsers, TestCase):
    method setUp (line 51) | def setUp(self):
    method test_lookups (line 55) | def test_lookups(self):
  class CampaignListFilterTests (line 75) | class CampaignListFilterTests(CreateTestUsers, TestCase):
    method setUp (line 76) | def setUp(self):
    method test_card_filter (line 79) | def test_card_filter(self):
    method test_project_filter (line 100) | def test_project_filter(self):
    method test_site_report_filter (line 123) | def test_site_report_filter(self):
  class ItemFilterTests (line 157) | class ItemFilterTests(CreateTestUsers, TestCase):
    method setUp (line 158) | def setUp(self):
    method test_project_filter (line 161) | def test_project_filter(self):
  class ProjectFilterTests (line 185) | class ProjectFilterTests(TestCase):
    method setUp (line 186) | def setUp(self):
    method test_project_campaign_status_list_filter (line 189) | def test_project_campaign_status_list_filter(self):
  class TranscriptionFilterTests (line 201) | class TranscriptionFilterTests(CreateTestUsers, TestCase):
    method setUp (line 202) | def setUp(self):
    method test_ocr_filter (line 206) | def test_ocr_filter(self):
  class TopicListFilterTests (line 218) | class TopicListFilterTests(TestCase):
    method setUp (line 219) | def setUp(self):
    method test_helpfullink_topic_list_filter (line 224) | def test_helpfullink_topic_list_filter(self):
  class NextAssetCampaignListFilterTests (line 236) | class NextAssetCampaignListFilterTests(TestCase):
    method setUp (line 237) | def setUp(self):
    method test_lookups_only_includes_used_campaigns (line 251) | def test_lookups_only_includes_used_campaigns(self):
  class SupersededListFilterTests (line 269) | class SupersededListFilterTests(CreateTestUsers, TestCase):
    method setUp (line 270) | def setUp(self):
    method test_lookups (line 284) | def test_lookups(self):
    method test_queryset_superseded_yes (line 293) | def test_queryset_superseded_yes(self):
    method test_queryset_superseded_no (line 304) | def test_queryset_superseded_no(self):
    method test_queryset_no_param_returns_all (line 312) | def test_queryset_no_param_returns_all(self):
    method test_queryset_ignores_unknown_value (line 318) | def test_queryset_ignores_unknown_value(self):

FILE: concordia/tests/test_admin_forms.py
  class SanitizedDescriptionAdminFormTests (line 7) | class SanitizedDescriptionAdminFormTests(TestCase):
    method test_clean (line 8) | def test_clean(self):
  class ClearCacheFormTests (line 25) | class ClearCacheFormTests(TestCase):
    method test_cache_name_choices (line 36) | def test_cache_name_choices(self):

FILE: concordia/tests/test_admin_views.py
  class TestProjectLevelExportView (line 33) | class TestProjectLevelExportView(CreateTestUsers, TestCase):
    method setUp (line 34) | def setUp(self):
    method test_get (line 49) | def test_get(self):
    method test_get_campaign (line 56) | def test_get_campaign(self):
    method test_post (line 64) | def test_post(self):
  class TestFunctionBasedViews (line 76) | class TestFunctionBasedViews(CreateTestUsers, TestCase, StreamingTestMix...
    method test_admin_bulk_import_review (line 77) | def test_admin_bulk_import_review(self):
    method test_admin_site_report_view (line 90) | def test_admin_site_report_view(self):
    method test_admin_retired_site_report_view (line 116) | def test_admin_retired_site_report_view(self):
  class TestAdminBulkImportView (line 138) | class TestAdminBulkImportView(CreateTestUsers, TestCase):
    method setUp (line 139) | def setUp(self):
    method test_get (line 162) | def test_get(self):
    method test_invalid_form (line 167) | def test_invalid_form(self):
    method test_fully_valid_form (line 172) | def test_fully_valid_form(self):
    method test_missing_field (line 236) | def test_missing_field(self):
    method test_empty_field (line 265) | def test_empty_field(self):
    method test_all_empty_fields (line 300) | def test_all_empty_fields(self):
    method test_empty_campaign_slug (line 326) | def test_empty_campaign_slug(self):
    method test_bad_campaign_slug (line 365) | def test_bad_campaign_slug(self):
    method test_empty_project_slug (line 398) | def test_empty_project_slug(self):
    method test_bad_project_slug (line 435) | def test_bad_project_slug(self):
    method test_bad_url (line 469) | def test_bad_url(self):
    method test_import_task_exception (line 498) | def test_import_task_exception(self):
  class TestAdminBulkChangeAssetStatus (line 529) | class TestAdminBulkChangeAssetStatus(CreateTestUsers, TestCase):
    method setUp (line 530) | def setUp(self):
    method test_admin_bulk_change_asset_status (line 573) | def test_admin_bulk_change_asset_status(self):
  class TestAdminBulkImportReview (line 610) | class TestAdminBulkImportReview(CreateTestUsers, TestCase):
    method setUp (line 611) | def setUp(self):
    method test_get (line 634) | def test_get(self):
    method test_invalid_form (line 639) | def test_invalid_form(self):
    method test_fully_valid_form (line 644) | def test_fully_valid_form(self):
    method test_missing_field (line 671) | def test_missing_field(self):
    method test_empty_field (line 702) | def test_empty_field(self):
    method test_all_empty_fields (line 740) | def test_all_empty_fields(self):
    method test_empty_campaign_slug (line 770) | def test_empty_campaign_slug(self):
    method test_bad_campaign_slug (line 800) | def test_bad_campaign_slug(self):
    method test_empty_project_slug (line 831) | def test_empty_project_slug(self):
    method test_bad_project_slug (line 862) | def test_bad_project_slug(self):
    method test_bad_url (line 893) | def test_bad_url(self):
    method test_large_number_urls (line 924) | def test_large_number_urls(self):
  class TestCeleryTaskReview (line 957) | class TestCeleryTaskReview(CreateTestUsers, TestCase):
    method setUp (line 958) | def setUp(self):
    method add_campaigns (line 964) | def add_campaigns(self):
    method add_active_campaigns (line 969) | def add_active_campaigns(self):
    method add_completed_campaigns (line 977) | def add_completed_campaigns(self):
    method add_retired_campaigns (line 989) | def add_retired_campaigns(self):
    method add_projects (line 1001) | def add_projects(self):
    method add_tasks (line 1053) | def add_tasks(self, campaign):
    method test_empty_dashboard (line 1095) | def test_empty_dashboard(self):
    method test_dashboard (line 1104) | def test_dashboard(self):
    method test_campaign_dashboard (line 1137) | def test_campaign_dashboard(self):
  class TestSerializedObjectView (line 1182) | class TestSerializedObjectView(TestCase):
    method setUp (line 1183) | def setUp(self):
    method test_exists (line 1188) | def test_exists(self):
    method test_dne (line 1197) | def test_dne(self):
  function mock_cache (line 1207) | def mock_cache(object_to_patch):
  class TestClearCacheView (line 1241) | class TestClearCacheView(CreateTestUsers, TestCase):
    method setUp (line 1242) | def setUp(self):
    method test_get (line 1246) | def test_get(self, caches_mock, cache_mock):
    method test_invalid_form (line 1253) | def test_invalid_form(self, caches_mock, cache_mock):
    method test_valid_form (line 1260) | def test_valid_form(self, caches_mock, cache_mock):
    method test_form_with_invalid_data (line 1268) | def test_form_with_invalid_data(self, caches_mock, cache_mock):
    method test_exception (line 1275) | def test_exception(self, caches_mock, cache_mock):

FILE: concordia/tests/test_api_views.py
  class URLAwareEncoderTest (line 30) | class URLAwareEncoderTest(TestCase):
    method test_default (line 31) | def test_default(self):
  class APIViewMixinTest (line 46) | class APIViewMixinTest(TestCase):
    method setUp (line 47) | def setUp(self):
    method test_serialize_conctext (line 50) | def test_serialize_conctext(self):
    method test_serialize_object (line 55) | def test_serialize_object(self, mtd_mock):
  class APIListViewTest (line 71) | class APIListViewTest(TestCase):
    method test_serialize_context (line 72) | def test_serialize_context(self, time_mock):
  class ConcordiaViewTests (line 82) | class ConcordiaViewTests(JSONAssertMixin, TestCase):
    method setUpTestData (line 84) | def setUpTestData(cls):
    method get_api_response (line 149) | def get_api_response(self, url, **request_args):
    method get_api_list_response (line 164) | def get_api_list_response(self, url, page_size=10, **request_args):
    method assertAbsoluteUrl (line 188) | def assertAbsoluteUrl(self, url, allow_none=True):
    method assertAbsoluteURLs (line 201) | def assertAbsoluteURLs(self, data):
    method assertAssetStatuses (line 216) | def assertAssetStatuses(self, asset_list, expected_statuses):
    method assertAssetsHaveLatestTranscriptions (line 226) | def assertAssetsHaveLatestTranscriptions(self, asset_list):
    method test_topic_detail (line 244) | def test_topic_detail(self):
    method test_campaign_list (line 272) | def test_campaign_list(self):
    method test_campaign_detail (line 289) | def test_campaign_detail(self):
    method test_project_detail (line 321) | def test_project_detail(self):
    method test_item_detail (line 363) | def test_item_detail(self):

FILE: concordia/tests/test_authentication.py
  class AuthenticationBackendTests (line 8) | class AuthenticationBackendTests(TestCase, CreateTestUsers):
    method test_EmailOrUsernameModelBackend (line 9) | def test_EmailOrUsernameModelBackend(self):

FILE: concordia/tests/test_celery.py
  class ConcordiaCeleryTests (line 11) | class ConcordiaCeleryTests(TestCase):
    method test_returns_early_for_non_package (line 12) | def test_returns_early_for_non_package(self):
    method test_imports_all_submodules_for_package (line 26) | def test_imports_all_submodules_for_package(self):
    method test_package_with_no_submodules (line 57) | def test_package_with_no_submodules(self):
    method test__load_all_task_modules_invokes_imports (line 76) | def test__load_all_task_modules_invokes_imports(self):
    method test_on_after_finalize_signal_triggers_handler (line 88) | def test_on_after_finalize_signal_triggers_handler(self):

FILE: concordia/tests/test_consumers.py
  class TestAssetConsumer (line 13) | class TestAssetConsumer(CreateTestUsers, TransactionTestCase):
    method test_asset_update (line 21) | async def test_asset_update(self):
    method test_asset_reservation_obtained (line 53) | async def test_asset_reservation_obtained(self):
    method test_asset_reservation_released (line 74) | async def test_asset_reservation_released(self):

FILE: concordia/tests/test_contextmanagers.py
  class CacheLockTests (line 7) | class CacheLockTests(TestCase):
    method setUp (line 8) | def setUp(self):
    method test_acquires_and_releases_lock (line 23) | def test_acquires_and_releases_lock(self):
    method test_does_not_release_if_lock_not_acquired (line 34) | def test_does_not_release_if_lock_not_acquired(self):
    method test_does_not_release_if_expired (line 42) | def test_does_not_release_if_expired(self):

FILE: concordia/tests/test_decorators.py
  class LockedTaskDecoratorTests (line 9) | class LockedTaskDecoratorTests(TestCase):
    method setUp (line 10) | def setUp(self):
    method make_task_instance (line 20) | def make_task_instance(self, name="test-task"):
    method test_lock_by_args_allows_only_one_execution (line 26) | def test_lock_by_args_allows_only_one_execution(self):
    method test_lock_by_task_name (line 48) | def test_lock_by_task_name(self):
    method test_force_runs_even_if_lock_not_acquired (line 65) | def test_force_runs_even_if_lock_not_acquired(self):
    method test_error_in_key_generation_logs_and_raises (line 82) | def test_error_in_key_generation_logs_and_raises(self):

FILE: concordia/tests/test_fields.py
  class TestFields (line 10) | class TestFields(TestCase):
    method test_TurnstileField (line 17) | def test_TurnstileField(self):

FILE: concordia/tests/test_logging.py
  class ConcordiaLoggerTests (line 10) | class ConcordiaLoggerTests(TestCase):
    method setUp (line 11) | def setUp(self):
    method test_debug_logs_with_event (line 15) | def test_debug_logs_with_event(self):
    method test_info_logs_with_event (line 23) | def test_info_logs_with_event(self):
    method test_warning_requires_reason_and_reason_code (line 31) | def test_warning_requires_reason_and_reason_code(self):
    method test_error_requires_reason_and_reason_code (line 51) | def test_error_requires_reason_and_reason_code(self):
    method test_missing_event_raises (line 69) | def test_missing_event_raises(self):
    method test_log_merges_context_correctly (line 73) | def test_log_merges_context_correctly(self):
    method test_log_explicit_key_overrides_extracted (line 80) | def test_log_explicit_key_overrides_extracted(self):
    method test_bind_merges_context (line 87) | def test_bind_merges_context(self):
    method test_bind_merges_context_into_logging (line 93) | def test_bind_merges_context_into_logging(self):
    method test_unregister_extractor_removes_extractor (line 100) | def test_unregister_extractor_removes_extractor(self):
    method test_register_extractor_warns_on_chained_override (line 105) | def test_register_extractor_warns_on_chained_override(self):
    method test_log_raises_when_message_is_none (line 119) | def test_log_raises_when_message_is_none(self):
    method test_log_raises_when_message_is_none_direct (line 123) | def test_log_raises_when_message_is_none_direct(self):
    method test_log_raises_when_message_is_empty (line 127) | def test_log_raises_when_message_is_empty(self):
    method test_log_raises_when_message_is_empty_direct (line 131) | def test_log_raises_when_message_is_empty_direct(self):
    method test_log_skips_none_values_from_extractor (line 135) | def test_log_skips_none_values_from_extractor(self):
    method test_log_includes_nonextractor_bound_context (line 145) | def test_log_includes_nonextractor_bound_context(self):
    method test_log_skips_none_values_in_context (line 152) | def test_log_skips_none_values_in_context(self):
    method test_log_overrides_bound_and_extracted_context (line 157) | def test_log_overrides_bound_and_extracted_context(self):
    method test_extractor_returns_none_value_skipped (line 167) | def test_extractor_returns_none_value_skipped(self):
    method test_get_logger_uses_structlog (line 174) | def test_get_logger_uses_structlog(self):
    method test_log_raises_valueerror_for_empty_reason_and_code (line 185) | def test_log_raises_valueerror_for_empty_reason_and_code(self):
    method test_exception_logs_with_exc_info (line 196) | def test_exception_logs_with_exc_info(self):

FILE: concordia/tests/test_maintenance.py
  class TestMaintenance (line 10) | class TestMaintenance(TestCase, CreateTestUsers):
    method setUp (line 11) | def setUp(self):
    method tearDown (line 15) | def tearDown(self):
    method test_need_maintenance_response_maintenance_default (line 18) | def test_need_maintenance_response_maintenance_default(self):
    method test_need_maintenance_response_maintenance_off (line 22) | def test_need_maintenance_response_maintenance_off(self):
    method test_need_maintenance_response_maintenance_on (line 27) | def test_need_maintenance_response_maintenance_on(self):
    method test_need_maintenance_response_maintenance_frontend (line 40) | def test_need_maintenance_response_maintenance_frontend(self):

FILE: concordia/tests/test_management_commands.py
  class EnsureInitialSiteConfigurationTests (line 10) | class EnsureInitialSiteConfigurationTests(TestCase):
    method test_command_output (line 11) | def test_command_output(self, *args, **kwargs):
  class ImportSiteReportsTests (line 30) | class ImportSiteReportsTests(TestCase):
    method test_command_output (line 31) | def test_command_output(self, *args, **kwargs):
  class PrintFrontendTestUrlsTests (line 41) | class PrintFrontendTestUrlsTests(TestCase):
    method test_command_output (line 42) | def test_command_output(self, *args, **kwargs):

FILE: concordia/tests/test_models.py
  class AssetTestCase (line 62) | class AssetTestCase(CreateTestUsers, TestCase):
    method setUp (line 63) | def setUp(self):
    method test_get_ocr_transcript (line 73) | def test_get_ocr_transcript(self):
    method test_get_contributor_count (line 83) | def test_get_contributor_count(self):
    method test_turn_off_ocr (line 86) | def test_turn_off_ocr(self):
    method test_get_storage_path (line 102) | def test_get_storage_path(self):
    method test_saving_without_campaign (line 108) | def test_saving_without_campaign(self):
    method test_rollforward_with_only_rollforward_transcriptions (line 120) | def test_rollforward_with_only_rollforward_transcriptions(self):
    method test_rollforward_with_too_many_rollforward_transcriptions (line 130) | def test_rollforward_with_too_many_rollforward_transcriptions(self):
    method test_rollforward_with_no_superseded_transcription (line 147) | def test_rollforward_with_no_superseded_transcription(self):
    method test_get_storage_path_handles_jpeg (line 163) | def test_get_storage_path_handles_jpeg(self):
  class ItemModelTests (line 169) | class ItemModelTests(TestCase):
    method test_thumbnail_link_prefers_image_url_when_present (line 170) | def test_thumbnail_link_prefers_image_url_when_present(self):
    method test_thumbnail_link_falls_back_when_image_url_raises (line 179) | def test_thumbnail_link_falls_back_when_image_url_raises(self):
    method test_thumbnail_link_returns_thumbnail_url_when_no_image (line 192) | def test_thumbnail_link_returns_thumbnail_url_when_no_image(self):
    method test_thumbnail_link_returns_none_when_no_image_or_url (line 198) | def test_thumbnail_link_returns_none_when_no_image_or_url(self):
  class TranscriptionManagerTestCase (line 205) | class TranscriptionManagerTestCase(CreateTestUsers, TestCase):
    method setUp (line 206) | def setUp(self):
    method test_recent_review_actions (line 215) | def test_recent_review_actions(self):
    method test_review_actions (line 227) | def test_review_actions(self):
    method test_review_incidents (line 232) | def test_review_incidents(self):
    method test_transcribe_incidents (line 304) | def test_transcribe_incidents(self):
    method test_review_incidents_returns_empty_when_counts_zero (line 350) | def test_review_incidents_returns_empty_when_counts_zero(self):
    method test_user_review_incidents_no_threshold_hit (line 370) | def test_user_review_incidents_no_threshold_hit(self):
    method test_review_incidents_no_threshold_match_inner_loop_break (line 394) | def test_review_incidents_no_threshold_match_inner_loop_break(self):
    method test_review_incidents_loops_until_threshold (line 417) | def test_review_incidents_loops_until_threshold(self):
  class TranscriptionTestCase (line 454) | class TranscriptionTestCase(CreateTestUsers, TestCase):
    method setUp (line 455) | def setUp(self):
    method test_campaign_slug (line 466) | def test_campaign_slug(self):
    method test_clean (line 471) | def test_clean(self):
    method test_save (line 495) | def test_save(self, mock_handler):
    method test_status (line 507) | def test_status(self):
  class SignalHandlersTest (line 534) | class SignalHandlersTest(CreateTestUsers, TestCase):
    method test_update_useractivity_cache (line 537) | def test_update_useractivity_cache(self, mock_set, mock_get):
  class AssetTranscriptionReservationTest (line 555) | class AssetTranscriptionReservationTest(CreateTestUsers, TestCase):
    method setUp (line 556) | def setUp(self):
    method test_get_token (line 566) | def test_get_token(self):
    method test_get_user (line 569) | def test_get_user(self):
  class UserProfileActivityTestCase (line 573) | class UserProfileActivityTestCase(TestCase):
    method setUp (line 574) | def setUp(self):
    method test_get_status (line 579) | def test_get_status(self):
    method test_total_actions (line 587) | def test_total_actions(self):
    method test_str (line 590) | def test_str(self):
  class UserProfileTestCase (line 595) | class UserProfileTestCase(CreateTestUsers, TestCase):
    method test_update_userprofileactivity_table (line 596) | def test_update_userprofileactivity_table(self):
    method test_update_userprofileactivity_table_updates_existing_and_profile (line 614) | def test_update_userprofileactivity_table_updates_existing_and_profile...
  class CampaignTestCase (line 642) | class CampaignTestCase(TestCase):
    method test_queryset (line 643) | def test_queryset(self):
  class CardTestCase (line 656) | class CardTestCase(TestCase):
    method test_str (line 657) | def test_str(self):
  class CardFamilyTestCase (line 662) | class CardFamilyTestCase(TestCase):
    method setUp (line 663) | def setUp(self):
    method test_str (line 666) | def test_str(self):
    method test_on_cardfamily_save (line 669) | def test_on_cardfamily_save(self):
  class HelpfulLinkTestCase (line 677) | class HelpfulLinkTestCase(TestCase):
    method setUp (line 678) | def setUp(self):
    method test_str (line 681) | def test_str(self):
    method test_queryset (line 684) | def test_queryset(self):
  class ConcordiaFileTestCase (line 692) | class ConcordiaFileTestCase(TestCase):
    method setUp (line 693) | def setUp(self):
    method test_str (line 696) | def test_str(self):
    method test_delete (line 699) | def test_delete(self):
    method test_concordia_file_upload_path (line 723) | def test_concordia_file_upload_path(self):
  class TagTestCase (line 741) | class TagTestCase(TestCase):
    method test_str (line 742) | def test_str(self):
  class UserAssetTagCollectionTestCase (line 747) | class UserAssetTagCollectionTestCase(TestCase):
    method test_str (line 748) | def test_str(self):
  class BannerTestCase (line 756) | class BannerTestCase(TestCase):
    method setUp (line 757) | def setUp(self):
    method test_str (line 760) | def test_str(self):
    method test_alert_class (line 763) | def test_alert_class(self):
    method test_btn_class (line 768) | def test_btn_class(self):
  class CarouselSlideTestCase (line 774) | class CarouselSlideTestCase(TestCase):
    method test_str (line 775) | def test_str(self):
  class CampaignRetirementProgressTestCase (line 780) | class CampaignRetirementProgressTestCase(TestCase):
    method test_str (line 781) | def test_str(self):
  class GuideTestCase (line 786) | class GuideTestCase(TestCase):
    method test_str (line 787) | def test_str(self):
  class SimplePageTestCase (line 792) | class SimplePageTestCase(TestCase):
    method test_str (line 793) | def test_str(self):
  class ValidatedGetOrCreateTestCase (line 798) | class ValidatedGetOrCreateTestCase(TestCase):
    method test_validated_get_or_create (line 799) | def test_validated_get_or_create(self):
  class NextAssetModelTests (line 811) | class NextAssetModelTests(TestCase):
    method setUp (line 812) | def setUp(self):
    method test_create_next_transcribable_campaign_asset (line 818) | def test_create_next_transcribable_campaign_asset(self):
    method test_create_next_reviewable_campaign_asset (line 831) | def test_create_next_reviewable_campaign_asset(self):
    method test_create_next_transcribable_topic_asset (line 844) | def test_create_next_transcribable_topic_asset(self):
    method test_create_next_reviewable_topic_asset (line 856) | def test_create_next_reviewable_topic_asset(self):
    method test_needed_for_campaign_respects_target_count (line 868) | def test_needed_for_campaign_respects_target_count(self):
    method test_needed_for_topic_respects_target_count (line 886) | def test_needed_for_topic_respects_target_count(self):
    method test_needed_for_campaign_raises_without_target (line 903) | def test_needed_for_campaign_raises_without_target(self):
    method test_needed_for_topic_raises_without_target (line 921) | def test_needed_for_topic_raises_without_target(self):
    method test_needed_for_campaign_with_explicit_target_count (line 939) | def test_needed_for_campaign_with_explicit_target_count(self):
    method test_needed_for_topic_with_explicit_target_count (line 959) | def test_needed_for_topic_with_explicit_target_count(self):
  class SiteReportAndManagerTestCase (line 978) | class SiteReportAndManagerTestCase(TestCase):
    method _aware (line 979) | def _aware(self, y, m, d, hh=12, mm=0, ss=0):
    method _mk_sr (line 983) | def _mk_sr(
    method test_calculate_assets_started (line 1002) | def test_calculate_assets_started(self):
    method test_series_navigation_and_sums (line 1030) | def test_series_navigation_and_sums(self):
    method test_per_campaign_and_topic_series_filters (line 1082) | def test_per_campaign_and_topic_series_filters(self):
    method test__series_filter_campaign_branch (line 1099) | def test__series_filter_campaign_branch(self):
    method test__series_filter_topic_branch (line 1120) | def test__series_filter_topic_branch(self):
    method test_series_filter_for_instance_topic_branch (line 1134) | def test_series_filter_for_instance_topic_branch(self):
    method test_series_filter_for_instance_retired_and_fallback (line 1149) | def test_series_filter_for_instance_retired_and_fallback(self):
    method test_to_debug_dict_includes_related_fields_and_counters (line 1161) | def test_to_debug_dict_includes_related_fields_and_counters(self):
    method test_first_on_or_after_with_upper_bound_campaign (line 1189) | def test_first_on_or_after_with_upper_bound_campaign(self):
    method test_previous_in_series_defaults_to_now (line 1203) | def test_previous_in_series_defaults_to_now(self):
    method test_sum_assets_started_treats_null_as_zero (line 1213) | def test_sum_assets_started_treats_null_as_zero(self):
    method test_to_debug_dict_campaign_status_and_topic_loop (line 1229) | def test_to_debug_dict_campaign_status_and_topic_loop(self):
    method test_to_debug_json_serializes_and_includes_counters (line 1251) | def test_to_debug_json_serializes_and_includes_counters(self):
    method test_first_on_or_after_without_upper_bound_topic (line 1268) | def test_first_on_or_after_without_upper_bound_topic(self):
    method test_to_debug_dict_skips_none_campaign_attrs (line 1284) | def test_to_debug_dict_skips_none_campaign_attrs(self):
    method test_to_debug_dict_skips_none_topic_attrs (line 1302) | def test_to_debug_dict_skips_none_topic_attrs(self):
  class KeyMetricsReportTestCase (line 1319) | class KeyMetricsReportTestCase(TestCase):
    method _aware (line 1320) | def _aware(self, y, m, d, hh=12, mm=0, ss=0):
    method _mk_sr (line 1324) | def _mk_sr(self, dt, report_name, **counters):
    method test_helpers (line 1332) | def test_helpers(self):
    method test_upsert_month_from_sitereports (line 1355) | def test_upsert_month_from_sitereports(self):
    method test_upsert_quarter_and_fiscal_year_rollups (line 1446) | def test_upsert_quarter_and_fiscal_year_rollups(self):
    method test___str___fallback_when_fields_incomplete (line 1511) | def test___str___fallback_when_fields_incomplete(self):
    method test_quarter_helper_edges (line 1536) | def test_quarter_helper_edges(self):
    method test__format_value_for_csv_variants (line 1545) | def test__format_value_for_csv_variants(self):
    method test_upsert_month_returns_none_when_no_snapshots (line 1569) | def test_upsert_month_returns_none_when_no_snapshots(self):
    method test_upsert_quarter_invalid_quarter_raises (line 1573) | def test_upsert_quarter_invalid_quarter_raises(self):
    method test_quarter_month_specs_all_quarters (line 1577) | def test_quarter_month_specs_all_quarters(self):
    method test_month_bounds_handles_december (line 1610) | def test_month_bounds_handles_december(self):
    method test__monthly_from_sitereports_returns_empty_dict_when_no_eom (line 1615) | def test__monthly_from_sitereports_returns_empty_dict_when_no_eom(self):
    method test__monthly_from_sitereports_baseline_fallback_inside_month (line 1622) | def test__monthly_from_sitereports_baseline_fallback_inside_month(self):
    method test__monthly_from_sitereports_treats_missing_series_as_zero (line 1657) | def test__monthly_from_sitereports_treats_missing_series_as_zero(self):
    method test_upsert_quarter_returns_none_when_no_monthlies_all_quarters (line 1680) | def test_upsert_quarter_returns_none_when_no_monthlies_all_quarters(se...
    method test_upsert_fiscal_year_returns_none_when_no_monthlies (line 1691) | def test_upsert_fiscal_year_returns_none_when_no_monthlies(self):
    method test__calendar_year_for_month_in_fy_helper (line 1695) | def test__calendar_year_for_month_in_fy_helper(self):
    method test_quarter_month_specs_q2 (line 1707) | def test_quarter_month_specs_q2(self):
  class KeyMetricsReportCsvTestCase (line 1718) | class KeyMetricsReportCsvTestCase(TestCase):
    method setUp (line 1719) | def setUp(self):
    method _csv_as_lines (line 1796) | def _csv_as_lines(self, rep: KeyMetricsReport) -> list[list[str]]:
    method test_monthly_csv_headers_and_values (line 1800) | def test_monthly_csv_headers_and_values(self):
    method test_quarterly_csv_headers_totals_and_lifetime (line 1822) | def test_quarterly_csv_headers_totals_and_lifetime(self):
    method test_fiscal_year_csv_headers_totals_and_lifetime (line 1855) | def test_fiscal_year_csv_headers_totals_and_lifetime(self):
    method test_str_formats (line 1891) | def test_str_formats(self):
    method test_quarterly_csv_when_no_monthlies_and_no_priors (line 1905) | def test_quarterly_csv_when_no_monthlies_and_no_priors(self):
    method test_fiscal_year_csv_headers_when_q1_missing (line 1935) | def test_fiscal_year_csv_headers_when_q1_missing(self):
    method test_format_value_for_csv_non_decimal_avg (line 1990) | def test_format_value_for_csv_non_decimal_avg(self):

FILE: concordia/tests/test_parser.py
  class ParserTestCase (line 42) | class ParserTestCase(TestCase):
    method test_extract_og_image (line 44) | def test_extract_og_image(self, mock_urlopen):
    method test_paginate_blog_posts (line 55) | def test_paginate_blog_posts(self, mock_urlopen, mock_extract_og_image):
    method test_get_http_error (line 74) | def test_get_http_error(self, mock_get, mock_logger):
    method test_get_exception_timeout (line 86) | def test_get_exception_timeout(self, mock_get, mock_logger):
    method test_get_connection_error (line 94) | def test_get_connection_error(self, mock_get, mock_logger):
    method test_get_request_exception (line 102) | def test_get_request_exception(self, mock_get, mock_logger):
    method test_ogimageparser_parses_meta_and_sets_og_image (line 110) | def test_ogimageparser_parses_meta_and_sets_og_image(self):
    method test_extract_og_image_request_exception_logs_and_returns_none (line 123) | def test_extract_og_image_request_exception_logs_and_returns_none(
    method test_get_og_image_calls_extract_on_cache_miss (line 136) | def test_get_og_image_calls_extract_on_cache_miss(
    method test_get_og_image_uses_cache_when_present (line 146) | def test_get_og_image_uses_cache_when_present(
    method _make_item_element (line 154) | def _make_item_element(self, title, link):
    method test_paginate_blog_posts_segments_and_includes_og_images (line 166) | def test_paginate_blog_posts_segments_and_includes_og_images(
    method test_paginate_blog_posts_with_no_items_returns_single_empty_segment (line 196) | def test_paginate_blog_posts_with_no_items_returns_single_empty_segment(

FILE: concordia/tests/test_registration_views.py
  class ConcordiaViewTests (line 30) | class ConcordiaViewTests(
    method test_send_activation_email_on_inactive_login (line 33) | def test_send_activation_email_on_inactive_login(self):
    method test_inactive_user_can_password_reset (line 45) | def test_inactive_user_can_password_reset(self):
    method test_password_reset_will_activate_user (line 53) | def test_password_reset_will_activate_user(self, signal_mock):
    method test_password_reset_with_activate_user (line 83) | def test_password_reset_with_activate_user(self, signal_mock):

FILE: concordia/tests/test_s3.py
  class S3StorageAPITest (line 10) | class S3StorageAPITest(TestCase):
    method setUp (line 11) | def setUp(self):
    method tearDown (line 19) | def tearDown(self):
    method test_s3_upload_api_layer (line 58) | def test_s3_upload_api_layer(self, mock_send, mock_add_auth):

FILE: concordia/tests/test_selenium.py
  class SeleniumTests (line 20) | class SeleniumTests(CreateTestUsers, StaticLiveServerTestCase):
    method setUpClass (line 22) | def setUpClass(cls):
    method tearDownClass (line 39) | def tearDownClass(cls):
    method reverse (line 43) | def reverse(self, name):
    method test_login (line 46) | def test_login(self):
    method test_blog_carousel (line 77) | def test_blog_carousel(self):

FILE: concordia/tests/test_sentry.py
  class TestSentry (line 10) | class TestSentry(TestCase):
    method test_sentry_config (line 18) | def test_sentry_config(self):

FILE: concordia/tests/test_signals.py
  class TestSignalHandlers (line 20) | class TestSignalHandlers(CreateTestUsers, TestCase):
    method setUp (line 21) | def setUp(self):
    method test_clear_reservation_token (line 26) | def test_clear_reservation_token(self):
    method test_user_successfully_activated (line 35) | def test_user_successfully_activated(self):
    method test_user_successfully_activated_no_request (line 44) | def test_user_successfully_activated_no_request(self):
    method test_user_successfully_activated_no_welcome_email (line 50) | def test_user_successfully_activated_no_welcome_email(self):
    method test_add_user_to_newsletter (line 59) | def test_add_user_to_newsletter(self):
  class UpdateAssetStatusSignalTests (line 80) | class UpdateAssetStatusSignalTests(CreateTestUsers, TestCase):
    method setUp (line 81) | def setUp(self):
    method test_accepted_transcription_sets_completed_status (line 86) | def test_accepted_transcription_sets_completed_status(self):
    method test_submitted_transcription_sets_submitted_status (line 92) | def test_submitted_transcription_sets_submitted_status(self):
    method test_rejected_transcription_sets_in_progress_status (line 100) | def test_rejected_transcription_sets_in_progress_status(self):
    method test_default_transcription_sets_in_progress_status (line 108) | def test_default_transcription_sets_in_progress_status(self):
    method test_outdated_transcription_does_not_update_status (line 116) | def test_outdated_transcription_does_not_update_status(self):
    method test_tasks_called_on_latest_transcription (line 141) | def test_tasks_called_on_latest_transcription(self, mock_calc, mock_re...
  class RequestIDHeaderTests (line 150) | class RequestIDHeaderTests(TestCase):
    method setUp (line 151) | def setUp(self):
    method tearDown (line 156) | def tearDown(self):
    method make_response (line 159) | def make_response(self, cache_control_header=None):
    method test_adds_header_when_no_cache_control (line 169) | def test_adds_header_when_no_cache_control(self, mock_contextvars):
    method test_adds_header_when_private (line 178) | def test_adds_header_when_private(self, mock_contextvars):
    method test_skips_header_when_public_with_max_age (line 187) | def test_skips_header_when_public_with_max_age(self, mock_contextvars):
    method test_adds_header_when_no_store_present (line 196) | def test_adds_header_when_no_store_present(self, mock_contextvars):

FILE: concordia/tests/test_tasks_assets.py
  class CalculateDifficultyValuesTests (line 23) | class CalculateDifficultyValuesTests(CreateTestUsers, TestCase):
    method setUp (line 24) | def setUp(self):
    method test_no_changes_when_difficulty_matches (line 32) | def test_no_changes_when_difficulty_matches(self):
    method test_updates_difficulty_for_explicit_queryset (line 40) | def test_updates_difficulty_for_explicit_queryset(self):
    method test_default_published_queryset_and_chunking (line 59) | def test_default_published_queryset_and_chunking(self):
  class PopulateAssetYearsTests (line 89) | class PopulateAssetYearsTests(TestCase):
    method setUp (line 90) | def setUp(self):
    method _set_metadata_dates (line 109) | def _set_metadata_dates(self, asset, *years):
    method test_updates_year_from_last_date_key (line 116) | def test_updates_year_from_last_date_key(self):
    method test_skips_when_year_unchanged (line 126) | def test_skips_when_year_unchanged(self):
    method test_multiple_assets_count_returned (line 134) | def test_multiple_assets_count_returned(self):
    method test_skips_empty_date_dicts_and_uses_last_year (line 150) | def test_skips_empty_date_dicts_and_uses_last_year(self):
  class FixStorageImagesTests (line 168) | class FixStorageImagesTests(TestCase):
    method setUp (line 169) | def setUp(self):
    method test_skips_when_storage_image_exists (line 197) | def test_skips_when_storage_image_exists(self):
    method test_downloads_and_saves_when_missing_success (line 210) | def test_downloads_and_saves_when_missing_success(self):
    method test_raises_and_logs_when_save_fails (line 244) | def test_raises_and_logs_when_save_fails(self):
    method test_filters_by_campaign_and_asset_start_id (line 273) | def test_filters_by_campaign_and_asset_start_id(self):
    method test_skips_when_storage_image_is_falsy (line 309) | def test_skips_when_storage_image_is_falsy(self):

FILE: concordia/tests/test_tasks_blog.py
  class BlogTaskTestCase (line 9) | class BlogTaskTestCase(TestCase):
    method test_fetch_and_cache_blog_images (line 12) | def test_fetch_and_cache_blog_images(self, mock_get, mock_extract):
    method test_skips_items_with_no_link (line 38) | def test_skips_items_with_no_link(self, mock_fetch, mock_extract):

FILE: concordia/tests/test_tasks_housekeeping.py
  class ClearSessionsTaskTests (line 8) | class ClearSessionsTaskTests(TestCase):
    method test_calls_django_clearsessions_command (line 9) | def test_calls_django_clearsessions_command(self):
    method test_raises_when_call_command_fails (line 16) | def test_raises_when_call_command_fails(self):

FILE: concordia/tests/test_tasks_next_asset.py
  class PopulateNextAssetTasksTests (line 36) | class PopulateNextAssetTasksTests(CreateTestUsers, TestCase):
    method setUp (line 37) | def setUp(self):
    method test_populate_next_transcribable_for_campaign (line 47) | def test_populate_next_transcribable_for_campaign(self):
    method test_populate_next_transcribable_for_topic (line 56) | def test_populate_next_transcribable_for_topic(self):
    method test_populate_next_reviewable_for_campaign (line 62) | def test_populate_next_reviewable_for_campaign(self):
    method test_populate_next_reviewable_for_topic (line 75) | def test_populate_next_reviewable_for_topic(self):
    method test_populate_next_transcribable_for_campaign_missing (line 88) | def test_populate_next_transcribable_for_campaign_missing(self, mock_l...
    method test_populate_next_transcribable_for_topic_missing (line 93) | def test_populate_next_transcribable_for_topic_missing(self, mock_logg...
    method test_populate_next_reviewable_for_campaign_missing (line 98) | def test_populate_next_reviewable_for_campaign_missing(self, mock_logg...
    method test_populate_next_reviewable_for_topic_missing (line 103) | def test_populate_next_reviewable_for_topic_missing(self, mock_logger):
    method test_populate_next_transcribable_for_campaign_none_needed (line 108) | def test_populate_next_transcribable_for_campaign_none_needed(self, mo...
    method test_populate_next_transcribable_for_topic_none_needed (line 127) | def test_populate_next_transcribable_for_topic_none_needed(self, mock_...
    method test_populate_next_reviewable_for_campaign_none_needed (line 146) | def test_populate_next_reviewable_for_campaign_none_needed(self, mock_...
    method test_populate_next_reviewable_for_topic_none_needed (line 170) | def test_populate_next_reviewable_for_topic_none_needed(self, mock_log...
    method test_populate_next_reviewable_for_campaign_none_found (line 194) | def test_populate_next_reviewable_for_campaign_none_found(self, mock_l...
    method test_populate_next_reviewable_for_topic_none_found (line 216) | def test_populate_next_reviewable_for_topic_none_found(self, mock_logg...
    method test_populate_next_transcribable_for_campaign_none_found (line 238) | def test_populate_next_transcribable_for_campaign_none_found(self, moc...
    method test_populate_next_transcribable_for_topic_none_found (line 257) | def test_populate_next_transcribable_for_topic_none_found(self, mock_l...
  class CleanNextAssetTasksTests (line 276) | class CleanNextAssetTasksTests(TestCase):
    method setUp (line 277) | def setUp(self):
    method test_clean_next_transcribable_for_campaign (line 323) | def test_clean_next_transcribable_for_campaign(self, mock_delay):
    method test_clean_next_transcribable_for_topic (line 337) | def test_clean_next_transcribable_for_topic(self, mock_delay):
    method test_clean_next_reviewable_for_campaign (line 349) | def test_clean_next_reviewable_for_campaign(self, mock_delay):
    method test_clean_next_reviewable_for_topic (line 361) | def test_clean_next_reviewable_for_topic(self, mock_delay):
    method test_renew_next_asset_cache (line 382) | def test_renew_next_asset_cache(
    method test_clean_next_transcribable_for_campaign_exception (line 396) | def test_clean_next_transcribable_for_campaign_exception(self, mock_lo...
    method test_clean_next_transcribable_for_topic_exception (line 408) | def test_clean_next_transcribable_for_topic_exception(self, mock_logger):
    method test_clean_next_reviewable_for_campaign_exception (line 420) | def test_clean_next_reviewable_for_campaign_exception(self, mock_logger):
    method test_clean_next_reviewable_for_topic_exception (line 432) | def test_clean_next_reviewable_for_topic_exception(self, mock_logger):

FILE: concordia/tests/test_tasks_reports_backfill.py
  class BackfillAssetsStartedTaskTests (line 10) | class BackfillAssetsStartedTaskTests(TestCase):
    method _dt (line 11) | def _dt(self, days_ago):
    method test_updates_total_and_skips_existing_by_default (line 14) | def test_updates_total_and_skips_existing_by_default(self):
    method test_recompute_when_skip_existing_is_false (line 108) | def test_recompute_when_skip_existing_is_false(self):
    method test_processes_retired_campaign_and_topic_series (line 175) | def test_processes_retired_campaign_and_topic_series(self):
    method test_skip_existing_branch_emits_heartbeat_due_to_time (line 216) | def test_skip_existing_branch_emits_heartbeat_due_to_time(self):
    method test_post_scan_heartbeat_emitted_due_to_time (line 268) | def test_post_scan_heartbeat_emitted_due_to_time(self):
    method test_no_update_when_equal_with_skip_existing_false (line 309) | def test_no_update_when_equal_with_skip_existing_false(self):
    method test_total_assets_started_is_rolled_up_from_campaign_series (line 345) | def test_total_assets_started_is_rolled_up_from_campaign_series(self):

FILE: concordia/tests/test_tasks_reports_key_metrics.py
  class BuildKeyMetricsReportsTaskTests (line 12) | class BuildKeyMetricsReportsTaskTests(TestCase):
    method _dt (line 13) | def _dt(self, days_ago):
    method test_recompute_all_calls_all_upserts (line 17) | def test_recompute_all_calls_all_upserts(self, mock_localdate):
    method test_incremental_refresh_and_creates (line 66) | def test_incremental_refresh_and_creates(self, mock_localdate):
    method test_early_return_after_backsteps (line 131) | def test_early_return_after_backsteps(self, mock_local, mock_sr, slog):
    method test_recompute_all_month_upsert_and_december_rollover (line 154) | def test_recompute_all_month_upsert_and_december_rollover(
    method test_incremental_month_create_and_refresh (line 185) | def test_incremental_month_create_and_refresh(
    method test_quarter_recompute_all_logs (line 232) | def test_quarter_recompute_all_logs(
    method test_quarter_incremental_refresh_all_quarters (line 272) | def test_quarter_incremental_refresh_all_quarters(
    method test_fiscal_year_recompute_all_logs (line 346) | def test_fiscal_year_recompute_all_logs(
    method test_fiscal_year_incremental_create_and_refresh (line 381) | def test_fiscal_year_incremental_create_and_refresh(
    method test_recompute_all_quarter_upserts_only (line 472) | def test_recompute_all_quarter_upserts_only(
    method test_incremental_quarter_refresh_only (line 517) | def test_incremental_quarter_refresh_only(
    method test_recompute_all_year_upsert_only (line 585) | def test_recompute_all_year_upsert_only(
    method test_incremental_year_create (line 624) | def test_incremental_year_create(
    method test_incremental_year_refresh (line 660) | def test_incremental_year_refresh(
    method test_quarter_recompute_all_upserts_and_continue (line 713) | def test_quarter_recompute_all_upserts_and_continue(
    method test_fiscal_year_recompute_all_upserts_and_continue (line 760) | def test_fiscal_year_recompute_all_upserts_and_continue(
    method test_quarter_recompute_all_non_none_continue_edge (line 803) | def test_quarter_recompute_all_non_none_continue_edge(
    method test_quarter_incremental_refresh_monthly_newer (line 847) | def test_quarter_incremental_refresh_monthly_newer(
    method test_fiscal_year_recompute_all_non_none_continue_edge (line 938) | def test_fiscal_year_recompute_all_non_none_continue_edge(
    method test_fiscal_year_incremental_create_missing (line 974) | def test_fiscal_year_incremental_create_missing(
    method test_fiscal_year_incremental_refresh_when_quarter_newer (line 1021) | def test_fiscal_year_incremental_refresh_when_quarter_newer(
    method test_quarter_recompute_all_none_branch_continue (line 1068) | def test_quarter_recompute_all_none_branch_continue(
    method test_quarter_incremental_refresh_none_branch_continue (line 1116) | def test_quarter_incremental_refresh_none_branch_continue(
    method test_fiscal_year_recompute_all_none_branch_continue (line 1190) | def test_fiscal_year_recompute_all_none_branch_continue(
    method test_fiscal_year_incremental_refresh_none_branch_continue (line 1241) | def test_fiscal_year_incremental_refresh_none_branch_continue(
    method test_incremental_fiscal_year_created_branch (line 1300) | def test_incremental_fiscal_year_created_branch(
    method test_incremental_fiscal_year_refresh_due_to_newer_quarter (line 1342) | def test_incremental_fiscal_year_refresh_due_to_newer_quarter(

FILE: concordia/tests/test_tasks_reports_sitereport.py
  class SiteReportTestCase (line 28) | class SiteReportTestCase(CreateTestUsers, TestCase):
    method setUpTestData (line 30) | def setUpTestData(cls):
    method test_daily_active_users (line 133) | def test_daily_active_users(self):
    method test_site_report (line 136) | def test_site_report(self):
    method test_retired_site_report (line 159) | def test_retired_site_report(self):
    method test_campaign_report (line 178) | def test_campaign_report(self):
    method test_topic_report (line 197) | def test_topic_report(self):
    method test_topic_report_zero_assets_emits_warning (line 215) | def test_topic_report_zero_assets_emits_warning(self):
  class SiteReportAssetsStartedRollupTests (line 236) | class SiteReportAssetsStartedRollupTests(CreateTestUsers, TestCase):
    method test_total_assets_started_rolls_up_campaign_deltas_ignoring_retirements (line 237) | def test_total_assets_started_rolls_up_campaign_deltas_ignoring_retire...
    method test_retired_total_assets_started_is_always_zero (line 320) | def test_retired_total_assets_started_is_always_zero(self):

FILE: concordia/tests/test_tasks_retirement.py
  class RetirementTasksTests (line 27) | class RetirementTasksTests(TestCase):
    method test_retire_campaign_initializes_totals_and_sets_status_and_triggers (line 28) | def test_retire_campaign_initializes_totals_and_sets_status_and_trigge...
    method test_retire_campaign_existing_progress_and_already_retired (line 54) | def test_retire_campaign_existing_progress_and_already_retired(self):
    method test_remove_next_project_calls_remove_next_item_when_project_exists (line 79) | def test_remove_next_project_calls_remove_next_item_when_project_exist...
    method test_remove_next_project_marks_complete_when_no_projects (line 89) | def test_remove_next_project_marks_complete_when_no_projects(self):
    method test_project_removal_success_increments_and_triggers_next (line 101) | def test_project_removal_success_increments_and_triggers_next(self):
    method test_remove_next_item_calls_remove_next_assets_when_item_exists (line 116) | def test_remove_next_item_calls_remove_next_assets_when_item_exists(se...
    method test_remove_next_item_deletes_project_and_triggers_when_no_items (line 128) | def test_remove_next_item_deletes_project_and_triggers_when_no_items(s...
    method test_assets_removal_success_updates_counts_and_triggers_next (line 141) | def test_assets_removal_success_updates_counts_and_triggers_next(self):
    method test_remove_next_assets_when_no_assets_deletes_item_and_triggers (line 158) | def test_remove_next_assets_when_no_assets_deletes_item_and_triggers(s...
    method test_remove_next_assets_with_assets_uses_chord_in_chunks_of_10 (line 173) | def test_remove_next_assets_with_assets_uses_chord_in_chunks_of_10(self):
    method test_delete_asset_deletes_storage_and_model_and_returns_id (line 210) | def test_delete_asset_deletes_storage_and_model_and_returns_id(self):
    method test_item_removal_success_increments_and_triggers_next (line 221) | def test_item_removal_success_increments_and_triggers_next(self):

FILE: concordia/tests/test_tasks_search_index.py
  class SearchIndexTasksTests (line 15) | class SearchIndexTasksTests(TestCase):
    method test_create_opensearch_indices_calls_management_command (line 16) | def test_create_opensearch_indices_calls_management_command(self):
    method test_delete_opensearch_indices_calls_management_command (line 29) | def test_delete_opensearch_indices_calls_management_command(self):
    method test_rebuild_opensearch_indices_calls_management_command (line 37) | def test_rebuild_opensearch_indices_calls_management_command(self):
    method test_populate_users_indices_calls_management_command (line 50) | def test_populate_users_indices_calls_management_command(self):
    method test_populate_assets_indices_calls_management_command (line 64) | def test_populate_assets_indices_calls_management_command(self):
    method test_populate_all_indices_calls_management_command (line 78) | def test_populate_all_indices_calls_management_command(self):

FILE: concordia/tests/test_tasks_thumbnails.py
  class ThumbnailsTasksTests (line 13) | class ThumbnailsTasksTests(TestCase):
    method test_download_item_thumbnail_task_returns_skip_when_no_url (line 14) | def test_download_item_thumbnail_task_returns_skip_when_no_url(self):
    method test_download_item_thumbnail_task_calls_helper_with_force (line 25) | def test_download_item_thumbnail_task_calls_helper_with_force(self):
    method test_download_missing_thumbnails_task_returns_zero_when_none (line 47) | def test_download_missing_thumbnails_task_returns_zero_when_none(self):
    method test_download_missing_thumbnails_task_filters_and_batches_once (line 55) | def test_download_missing_thumbnails_task_filters_and_batches_once(self):
    method test_download_missing_thumbnails_task_multiple_waves (line 118) | def test_download_missing_thumbnails_task_multiple_waves(self):

FILE: concordia/tests/test_tasks_unusualactivity.py
  class UnusualActivityTaskTests (line 10) | class UnusualActivityTaskTests(TestCase):
    method test_noop_when_not_production_and_not_ignored (line 12) | def test_noop_when_not_production_and_not_ignored(self):
    method test_runs_in_production_without_default_to (line 38) | def test_runs_in_production_without_default_to(self):
    method test_ignore_env_appends_suffix_and_includes_default_to (line 107) | def test_ignore_env_appends_suffix_and_includes_default_to(self):

FILE: concordia/tests/test_tasks_useractivity.py
  class UserActivityTaskTestCase (line 30) | class UserActivityTaskTestCase(CreateTestUsers, TestCase):
    method setUp (line 31) | def setUp(self):
    method test_update_userprofileactivity_from_cache_no_updates (line 38) | def test_update_userprofileactivity_from_cache_no_updates(self, mock_u...
    method test_update_userprofileactivity_from_cache_update (line 51) | def test_update_userprofileactivity_from_cache_update(self, mock_updat...
    method test_unusual_activity (line 64) | def test_unusual_activity(self, mock_transcription):
    method test_update_useractivity_cache (line 77) | def test_update_useractivity_cache(self, mock_update, mock_delete, moc...
    method test_populate_active_campaign_counts_computes_user_and_anon_rows (line 100) | def test_populate_active_campaign_counts_computes_user_and_anon_rows(s...
    method test_populate_completed_campaign_counts_processes_non_active_only (line 146) | def test_populate_completed_campaign_counts_processes_non_active_only(...
    method test_update_useractivity_cache_lock_max_retries_sends_email (line 166) | def test_update_useractivity_cache_lock_max_retries_sends_email(self):
    method test_update_useractivity_cache_update_exception_releases_lock (line 194) | def test_update_useractivity_cache_update_exception_releases_lock(self):
  class UpdateUserprofileactivityFromCacheTestCase (line 211) | class UpdateUserprofileactivityFromCacheTestCase(CreateTestUsers, TestCa...
    method setUp (line 212) | def setUp(self):
    method test_no_updates (line 219) | def test_no_updates(self, mock_update_table):
    method test_update (line 232) | def test_update(self, mock_update_table):

FILE: concordia/tests/test_tasks_visualizations.py
  class VisualizationCacheTasksTests (line 27) | class VisualizationCacheTasksTests(TestCase):
    class _UploadFailed (line 28) | class _UploadFailed(Exception):
    method setUp (line 31) | def setUp(self):
    method test_populate_asset_status_visualization_cache (line 35) | def test_populate_asset_status_visualization_cache(self):
    method test_populate_daily_activity_visualization_cache (line 70) | def test_populate_daily_activity_visualization_cache(self):
    method test_negative_daily_saved_clamps_to_zero (line 108) | def test_negative_daily_saved_clamps_to_zero(self):
    method test_asset_status_unchanged_skips_upload_and_cache_update (line 137) | def test_asset_status_unchanged_skips_upload_and_cache_update(self):
    method test_asset_status_upload_failure_with_prior_url_falls_back (line 182) | def test_asset_status_upload_failure_with_prior_url_falls_back(self):
    method test_asset_status_upload_failure_without_prior_url_raises (line 226) | def test_asset_status_upload_failure_without_prior_url_raises(self):
    method test_daily_activity_unchanged_skips_upload_and_cache_update (line 249) | def test_daily_activity_unchanged_skips_upload_and_cache_update(self):
    method test_daily_activity_upload_failure_with_prior_url_falls_back (line 280) | def test_daily_activity_upload_failure_with_prior_url_falls_back(self):
    method test_daily_activity_upload_failure_without_prior_url_raises (line 333) | def test_daily_activity_upload_failure_without_prior_url_raises(self):

FILE: concordia/tests/test_templatetags.py
  class TestTemplateTags (line 19) | class TestTemplateTags(TestCase):
    method test_truncatechars_on_word_break (line 20) | def test_truncatechars_on_word_break(self):
    method test_multiply (line 40) | def test_multiply(self):
    method test_transcription_status_filters (line 45) | def test_transcription_status_filters(self):
    method test_qs_alter (line 54) | def test_qs_alter(self):
    method test_reprchar_variants (line 99) | def test_reprchar_variants(self):
  class RejectFilterTests (line 111) | class RejectFilterTests(TestCase):
    method test_returns_input_when_falsy (line 112) | def test_returns_input_when_falsy(self):
    method test_string_single_reject (line 118) | def test_string_single_reject(self):
    method test_string_multiple_rejects (line 124) | def test_string_multiple_rejects(self):
    method test_string_no_match (line 130) | def test_string_no_match(self):
    method test_string_empty_args (line 133) | def test_string_empty_args(self):
    method test_string_whitespace_split_and_join (line 136) | def test_string_whitespace_split_and_join(self):
    method test_string_case_sensitivity (line 139) | def test_string_case_sensitivity(self):
    method test_iterable_list (line 142) | def test_iterable_list(self):
    method test_iterable_tuple_and_duplicates (line 148) | def test_iterable_tuple_and_duplicates(self):
    method test_iterable_no_match (line 151) | def test_iterable_no_match(self):
    method test_iterable_empty_args (line 154) | def test_iterable_empty_args(self):
  class VisualizationTagsTests (line 167) | class VisualizationTagsTests(TestCase):
    method test_without_attrs_renders_section_and_script (line 168) | def test_without_attrs_renders_section_and_script(self):
    method test_with_attrs_and_escaping (line 177) | def test_with_attrs_and_escaping(self):
    method test_name_escaping_in_id_and_script_src (line 193) | def test_name_escaping_in_id_and_script_src(self):

FILE: concordia/tests/test_top_level_views.py
  class TopLevelViewTests (line 31) | class TopLevelViewTests(
    method setUp (line 34) | def setUp(self):
    method tearDown (line 37) | def tearDown(self):
    method test_healthz (line 40) | def test_healthz(self):
    method test_homepage (line 52) | def test_homepage(self):
    method test_contact_us_redirect (line 81) | def test_contact_us_redirect(self):
    method test_simple_page (line 87) | def test_simple_page(self):
    method test_nested_simple_page (line 121) | def test_nested_simple_page(self):
    method test_simple_page_with_context (line 150) | def test_simple_page_with_context(self):
  class HelpCenterRedirectTests (line 216) | class HelpCenterRedirectTests(TestCase):
    method test_HelpCenterRedirectView (line 217) | def test_HelpCenterRedirectView(self):
    method test_HelpCenterSpanishRedirectView (line 228) | def test_HelpCenterSpanishRedirectView(self):
  class MaintenanceModeTests (line 240) | class MaintenanceModeTests(TestCase, CreateTestUsers):
    method setUp (line 241) | def setUp(self):
    method tearDown (line 246) | def tearDown(self):
    method test_maintenance_mode_off (line 249) | def test_maintenance_mode_off(self):
    method test_maintenance_mode_on_without_frontend (line 274) | def test_maintenance_mode_on_without_frontend(self):
    method test_maintenance_mode_on_with_frontend (line 300) | def test_maintenance_mode_on_with_frontend(self):
    method test_maintenance_mode_frontend_available (line 325) | def test_maintenance_mode_frontend_available(self):
    method test_maintenance_mode_frontend_unavailable (line 348) | def test_maintenance_mode_frontend_unavailable(self):

FILE: concordia/tests/test_utils_celery.py
  class CeleryUtilsTests (line 9) | class CeleryUtilsTests(TestCase):
    method test_get_registered_task_returns_task_from_registry (line 10) | def test_get_registered_task_returns_task_from_registry(self):
    method test_get_registered_task_raises_runtime_error_with_cause (line 24) | def test_get_registered_task_raises_runtime_error_with_cause(self):

FILE: concordia/tests/test_utils_logging.py
  class LoggingTests (line 11) | class LoggingTests(CreateTestUsers, TestCase):
    method test_get_logging_user_id_authenticated_user (line 12) | def test_get_logging_user_id_authenticated_user(self):
    method test_get_logging_user_id_anonymous_user (line 16) | def test_get_logging_user_id_anonymous_user(self):
    method test_get_logging_user_id_missing_auth_attribute (line 20) | def test_get_logging_user_id_missing_auth_attribute(self):
    method test_get_logging_user_id_authenticated_no_id (line 24) | def test_get_logging_user_id_authenticated_no_id(self):

FILE: concordia/tests/test_utils_next_asset_reviewable_campaign.py
  class NextReviewableCampaignAssetTests (line 36) | class NextReviewableCampaignAssetTests(CreateTestUsers, TestCase):
    method setUp (line 37) | def setUp(self):
    method test_find_new_reviewable_campaign_assets_filters_correctly (line 46) | def test_find_new_reviewable_campaign_assets_filters_correctly(self):
    method test_find_new_reviewable_campaign_assets_without_user (line 52) | def test_find_new_reviewable_campaign_assets_without_user(self):
    method test_find_reviewable_campaign_asset_from_next_table (line 58) | def test_find_reviewable_campaign_asset_from_next_table(self):
    method test_find_reviewable_campaign_asset_falls_back_and_spawns_task (line 76) | def test_find_reviewable_campaign_asset_falls_back_and_spawns_task(
    method test_find_next_reviewable_campaign_asset_orders_and_falls_back (line 89) | def test_find_next_reviewable_campaign_asset_orders_and_falls_back(
    method test_find_next_reviewable_campaign_asset_when_next_asset_exists (line 113) | def test_find_next_reviewable_campaign_asset_when_next_asset_exists(
    method test_short_circuit_same_item_excludes_users_own_work (line 142) | def test_short_circuit_same_item_excludes_users_own_work(self):
    method test_item_short_circuit_reviewable_respects_after_sequence_and_reservations (line 162) | def test_item_short_circuit_reviewable_respects_after_sequence_and_res...
    method test_project_short_circuit_when_item_has_only_users_work (line 199) | def test_project_short_circuit_when_item_has_only_users_work(self):
    method test_cache_excludes_user_and_triggers_spawn_task (line 223) | def test_cache_excludes_user_and_triggers_spawn_task(self, mock_get_ta...
  class ReviewableCampaignInternalsTests (line 261) | class ReviewableCampaignInternalsTests(CreateTestUsers, TestCase):
    method setUp (line 262) | def setUp(self):
    method test_reserved_asset_ids_subq_filters_to_campaign (line 270) | def test_reserved_asset_ids_subq_filters_to_campaign(self):
    method test_eligible_reviewable_base_qs_excludes_user_and_requires_submitted (line 290) | def test_eligible_reviewable_base_qs_excludes_user_and_requires_submit...
    method test_next_seq_after_none_missing_and_valid (line 305) | def test_next_seq_after_none_missing_and_valid(self):
    method test_find_reviewable_in_item_after_none_returns_first (line 310) | def test_find_reviewable_in_item_after_none_returns_first(self):
    method test_find_reviewable_in_item_after_asset_in_other_item_ignores_gate (line 322) | def test_find_reviewable_in_item_after_asset_in_other_item_ignores_gat...
    method test_find_reviewable_in_item_after_asset_missing_ignores_gate (line 340) | def test_find_reviewable_in_item_after_asset_missing_ignores_gate(self):
    method test_find_reviewable_in_item_after_asset_sidc_ignores_gate (line 352) | def test_find_reviewable_in_item_after_asset_sidc_ignores_gate(self):
    method test_find_reviewable_in_project_orders_and_excludes_user (line 376) | def test_find_reviewable_in_project_orders_and_excludes_user(self):
    method test_find_reviewable_in_project_returns_none_when_only_users_work (line 391) | def test_find_reviewable_in_project_returns_none_when_only_users_work(...
    method test_find_new_reviewable_campaign_assets_excludes_reserved_and_next_table (line 404) | def test_find_new_reviewable_campaign_assets_excludes_reserved_and_nex...
    method test_find_and_order_potential_reviewable_campaign_assets_ordering (line 437) | def test_find_and_order_potential_reviewable_campaign_assets_ordering(...
    method test_find_reviewable_campaign_asset_no_eligible_spawns_task_and_returns_none (line 491) | def test_find_reviewable_campaign_asset_no_eligible_spawns_task_and_re...
    method test_manual_fallback_orders_and_spawns_task (line 503) | def test_manual_fallback_orders_and_spawns_task(self, mock_get_task):
    method test_find_invalid_next_reviewable_campaign_assets_reserved_and_wrong_status (line 523) | def test_find_invalid_next_reviewable_campaign_assets_reserved_and_wro...
    method test_item_short_circuit_internal_applies_after_and_skips_reserved (line 569) | def test_item_short_circuit_internal_applies_after_and_skips_reserved(...
    method test_item_short_circuit_internal_excludes_users_own_work (line 587) | def test_item_short_circuit_internal_excludes_users_own_work(self):
    method test_project_short_circuit_internal_skips_reserved_first (line 601) | def test_project_short_circuit_internal_skips_reserved_first(self):
    method test_order_potential_without_after_prefers_item_then_project (line 620) | def test_order_potential_without_after_prefers_item_then_project(self):
    method test_next_reviewable_manual_fallback_no_after_spawns_and_picks_lowest_seq (line 687) | def test_next_reviewable_manual_fallback_no_after_spawns_and_picks_low...
    method test_next_reviewable_manual_fallback_invalid_after_str (line 710) | def test_next_reviewable_manual_fallback_invalid_after_str(self, mock_...
    method test_next_reviewable_cached_path_when_short_circuits_fail (line 731) | def test_next_reviewable_cached_path_when_short_circuits_fail(self, mo...
    method test_next_reviewable_uses_cache_when_bypassing_short_circuits (line 773) | def test_next_reviewable_uses_cache_when_bypassing_short_circuits(

FILE: concordia/tests/test_utils_next_asset_reviewable_topic.py
  class NextReviewableTopicAssetTests (line 50) | class NextReviewableTopicAssetTests(CreateTestUsers, TestCase):
    method setUp (line 51) | def setUp(self):
    method test_find_new_reviewable_topic_assets_filters_correctly (line 60) | def test_find_new_reviewable_topic_assets_filters_correctly(self):
    method test_find_new_reviewable_topic_assets_without_user (line 66) | def test_find_new_reviewable_topic_assets_without_user(self):
    method test_find_reviewable_topic_asset_from_next_table (line 72) | def test_find_reviewable_topic_asset_from_next_table(self):
    method test_find_reviewable_topic_asset_falls_back_and_spawns_task (line 90) | def test_find_reviewable_topic_asset_falls_back_and_spawns_task(
    method test_find_next_reviewable_topic_asset_orders_and_falls_back (line 103) | def test_find_next_reviewable_topic_asset_orders_and_falls_back(
    method test_find_next_reviewable_topic_asset_when_next_asset_exists (line 127) | def test_find_next_reviewable_topic_asset_when_next_asset_exists(
    method test_short_circuit_same_item_topic_excludes_users_own_work (line 156) | def test_short_circuit_same_item_topic_excludes_users_own_work(self):
    method test_item_short_circuit_topic_reviewable_respects_after_and_reservations (line 171) | def test_item_short_circuit_topic_reviewable_respects_after_and_reserv...
    method test_cache_excludes_user_and_triggers_spawn_task_topic (line 198) | def test_cache_excludes_user_and_triggers_spawn_task_topic(self, mock_...
    method test_find_next_reviewable_topic_assets_excludes_user (line 228) | def test_find_next_reviewable_topic_assets_excludes_user(self):
    method test_next_reviewable_cached_path_when_short_circuits_fail_topic (line 255) | def test_next_reviewable_cached_path_when_short_circuits_fail_topic(
    method test_next_reviewable_uses_cache_when_bypassing_short_circuits_topic (line 302) | def test_next_reviewable_uses_cache_when_bypassing_short_circuits_topic(
    method test_next_reviewable_manual_fallback_no_after_spawns_and_picks_lowest_seq_topic (line 343) | def test_next_reviewable_manual_fallback_no_after_spawns_and_picks_low...
    method test_next_reviewable_manual_fallback_invalid_after_str_topic (line 367) | def test_next_reviewable_manual_fallback_invalid_after_str_topic(
  class ReviewableTopicInternalsTests (line 393) | class ReviewableTopicInternalsTests(CreateTestUsers, TestCase):
    method setUp (line 394) | def setUp(self):
    method test_topic_reserved_asset_ids_subq_unfiltered (line 401) | def test_topic_reserved_asset_ids_subq_unfiltered(self):
    method test_topic_eligible_reviewable_base_qs_excludes_user_and_requires_submitted (line 423) | def test_topic_eligible_reviewable_base_qs_excludes_user_and_requires_...
    method test_topic_next_seq_after_none_missing_and_valid (line 441) | def test_topic_next_seq_after_none_missing_and_valid(self):
    method test_topic_find_reviewable_in_item_after_none_returns_first (line 446) | def test_topic_find_reviewable_in_item_after_none_returns_first(self):
    method test_topic_find_reviewable_in_item_after_asset_in_other_item_ignores_gate (line 458) | def test_topic_find_reviewable_in_item_after_asset_in_other_item_ignor...
    method test_topic_find_reviewable_in_item_after_asset_missing_ignores_gate (line 478) | def test_topic_find_reviewable_in_item_after_asset_missing_ignores_gat...
    method test_topic_find_reviewable_in_item_after_asset_sidc_ignores_gate (line 490) | def test_topic_find_reviewable_in_item_after_asset_sidc_ignores_gate(s...
    method test_topic_find_reviewable_in_project_orders_and_excludes_user (line 512) | def test_topic_find_reviewable_in_project_orders_and_excludes_user(self):
    method test_topic_find_reviewable_in_project_returns_none_when_only_users_work (line 528) | def test_topic_find_reviewable_in_project_returns_none_when_only_users...
    method test_find_and_order_potential_reviewable_topic_assets_ordering (line 542) | def test_find_and_order_potential_reviewable_topic_assets_ordering(self):
    method test_order_potential_without_after_prefers_item_then_project_topic (line 585) | def test_order_potential_without_after_prefers_item_then_project_topic...
    method test_find_invalid_next_reviewable_topic_assets_reserved_and_wrong_status (line 627) | def test_find_invalid_next_reviewable_topic_assets_reserved_and_wrong_...
    method test_topic_project_short_circuit_internal_skips_reserved_first (line 673) | def test_topic_project_short_circuit_internal_skips_reserved_first(self):
    method test_topic_item_short_circuit_internal_excludes_users_own_work (line 692) | def test_topic_item_short_circuit_internal_excludes_users_own_work(self):
    method test_topic_item_short_circuit_internal_applies_after_and_skips_reserved (line 706) | def test_topic_item_short_circuit_internal_applies_after_and_skips_res...

FILE: concordia/tests/test_utils_next_asset_transcribable_campaign.py
  class NextTranscribableCampaignAssetTests (line 54) | class NextTranscribableCampaignAssetTests(CreateTestUsers, TestCase):
    method setUp (line 55) | def setUp(self):
    method test_find_new_transcribable_campaign_assets_filters_correctly (line 66) | def test_find_new_transcribable_campaign_assets_filters_correctly(self):
    method test_find_transcribable_campaign_asset_from_next_table (line 77) | def test_find_transcribable_campaign_asset_from_next_table(self):
    method test_find_transcribable_campaign_asset_falls_back_and_spawns_task (line 93) | def test_find_transcribable_campaign_asset_falls_back_and_spawns_task(
    method test_find_next_transcribable_campaign_asset_orders_and_falls_back (line 105) | def test_find_next_transcribable_campaign_asset_orders_and_falls_back(
    method test_find_next_transcribable_campaign_asset_when_next_asset_exists (line 127) | def test_find_next_transcribable_campaign_asset_when_next_asset_exists(
    method test_short_circuit_same_item_respects_after_sequence_and_reservations (line 161) | def test_short_circuit_same_item_respects_after_sequence_and_reservati...
    method test_project_short_circuit_prefers_not_started_over_in_progress (line 192) | def test_project_short_circuit_prefers_not_started_over_in_progress(se...
    method test_project_short_circuit_when_item_id_empty_string (line 222) | def test_project_short_circuit_when_item_id_empty_string(self):
    method test_find_transcribable_campaign_asset_none_spawns (line 243) | def test_find_transcribable_campaign_asset_none_spawns(self, mock_get_...
    method test_next_transcribable_manual_no_after_prefers_not_started (line 259) | def test_next_transcribable_manual_no_after_prefers_not_started(
    method test_next_transcribable_manual_invalid_after_str (line 290) | def test_next_transcribable_manual_invalid_after_str(self, mock_get_ta...
    method test_next_transcribable_none_anywhere_spawns (line 320) | def test_next_transcribable_none_anywhere_spawns(self, mock_get_task):
    method test_item_short_circuit_missing_after_pk_treated_as_none_top (line 336) | def test_item_short_circuit_missing_after_pk_treated_as_none_top(self):
    method test_cache_excludes_original_pk_and_chooses_next (line 349) | def test_cache_excludes_original_pk_and_chooses_next(self, mock_get_ta...
    method test_project_short_circuit_without_original_id (line 382) | def test_project_short_circuit_without_original_id(self):
    method test_item_short_circuit_after_pk_in_other_item_ignores_gate (line 401) | def test_item_short_circuit_after_pk_in_other_item_ignores_gate(self):
    method test_next_transcribable_after_pk_missing_treats_as_no_after (line 417) | def test_next_transcribable_after_pk_missing_treats_as_no_after(self):
    method test_no_ns_anywhere_and_no_ip_in_item_returns_none (line 430) | def test_no_ns_anywhere_and_no_ip_in_item_returns_none(self, mock_get_...
    method test_after_pk_digit_string_missing_treats_as_no_after (line 452) | def test_after_pk_digit_string_missing_treats_as_no_after(self):
    method test_same_item_inprogress_selected_when_no_ns_and_no_after (line 465) | def test_same_item_inprogress_selected_when_no_ns_and_no_after(self):
    method test_manual_invalid_after_str_campaign_valueerror_branch (line 482) | def test_manual_invalid_after_str_campaign_valueerror_branch(self, moc...
  class TranscribableCampaignInternalsTests (line 502) | class TranscribableCampaignInternalsTests(CreateTestUsers, TestCase):
    method setUp (line 503) | def setUp(self):
    method test_new_transcribable_excludes_reserved_and_cached (line 510) | def test_new_transcribable_excludes_reserved_and_cached(self):
    method test_order_potential_transcribable_pref (line 533) | def test_order_potential_transcribable_pref(self):
    method test_order_potential_transcribable_no_after (line 576) | def test_order_potential_transcribable_no_after(self):
    method test_invalid_next_transcribable_reserved_and_submitted (line 619) | def test_invalid_next_transcribable_reserved_and_submitted(self):
  class TranscribableCampaignMoreInternalsTests (line 662) | class TranscribableCampaignMoreInternalsTests(CreateTestUsers, TestCase):
    method setUp (line 663) | def setUp(self):
    method test_tc_reserved_ids_filters_to_campaign (line 672) | def test_tc_reserved_ids_filters_to_campaign(self):
    method test_tc_next_seq_after_variants (line 692) | def test_tc_next_seq_after_variants(self):
    method test_tc_order_unstarted_first_prefers_not_started (line 697) | def test_tc_order_unstarted_first_prefers_not_started(self):
    method test_find_in_item_after_none_returns_first_not_started (line 704) | def test_find_in_item_after_none_returns_first_not_started(self):
    method test_find_in_item_skips_inprog_and_reserved_and_advances (line 709) | def test_find_in_item_skips_inprog_and_reserved_and_advances(self):
    method test_find_in_item_after_missing_excludes_id_only (line 722) | def test_find_in_item_after_missing_excludes_id_only(self):
    method test_find_ns_in_proj_excludes_item_and_reserved (line 729) | def test_find_ns_in_proj_excludes_item_and_reserved(self):
    method test_find_ns_in_proj_blank_slug_none (line 746) | def test_find_ns_in_proj_blank_slug_none(self):
    method test_cache_same_item_is_ignored_then_manual_selects (line 750) | def test_cache_same_item_is_ignored_then_manual_selects(self, mock_get...
    method test_manual_excludes_original_pk_and_same_item (line 785) | def test_manual_excludes_original_pk_and_same_item(self, mock_get_task):
    method test_same_item_inprog_after_when_no_not_started (line 809) | def test_same_item_inprog_after_when_no_not_started(self):
    method test_eligible_base_qs_filters_status_and_published (line 824) | def test_eligible_base_qs_filters_status_and_published(self):
    method test_cached_transcribable_accessor_returns_rows (line 848) | def test_cached_transcribable_accessor_returns_rows(self):
    method test_find_in_item_blank_item_id_none (line 862) | def test_find_in_item_blank_item_id_none(self):
    method test_find_ns_in_proj_without_exclude_includes_same_item (line 866) | def test_find_ns_in_proj_without_exclude_includes_same_item(self):

FILE: concordia/tests/test_utils_next_asset_transcribable_topic.py
  class NextTranscribableTopicAssetTests (line 57) | class NextTranscribableTopicAssetTests(CreateTestUsers, TestCase):
    method setUp (line 58) | def setUp(self):
    method test_find_new_transcribable_topic_assets_filters_correctly (line 72) | def test_find_new_transcribable_topic_assets_filters_correctly(self):
    method test_find_transcribable_topic_asset_from_next_table (line 84) | def test_find_transcribable_topic_asset_from_next_table(self):
    method test_find_transcribable_topic_asset_falls_back_and_spawns_task (line 100) | def test_find_transcribable_topic_asset_falls_back_and_spawns_task(
    method test_find_next_transcribable_topic_asset_orders_and_falls_back (line 112) | def test_find_next_transcribable_topic_asset_orders_and_falls_back(
    method test_find_next_transcribable_topic_asset_when_next_asset_exists (line 134) | def test_find_next_transcribable_topic_asset_when_next_asset_exists(
    method test_short_circuit_same_item_topic_respects_after_sequence_and_reservations (line 162) | def test_short_circuit_same_item_topic_respects_after_sequence_and_res...
    method test_project_short_circuit_topic_prefers_not_started_over_in_progress (line 191) | def test_project_short_circuit_topic_prefers_not_started_over_in_progr...
    method test_project_short_circuit_topic_without_item_id_allows_same_item (line 218) | def test_project_short_circuit_topic_without_item_id_allows_same_item(
  class TranscribableTopicInternalsTests (line 241) | class TranscribableTopicInternalsTests(CreateTestUsers, TestCase):
    method setUp (line 242) | def setUp(self):
    method test_topic_transcribable_reserved_ids_is_unfiltered (line 249) | def test_topic_transcribable_reserved_ids_is_unfiltered(self):
    method test_topic_transcribable_eligible_base_qs_filters_correctly (line 268) | def test_topic_transcribable_eligible_base_qs_filters_correctly(self):
    method test_topic_next_seq_after_variants_for_transcribable (line 292) | def test_topic_next_seq_after_variants_for_transcribable(self):
    method test_topic_find_in_item_for_topic_after_none_returns_first_not_started (line 300) | def test_topic_find_in_item_for_topic_after_none_returns_first_not_sta...
    method test_topic_find_in_item_for_topic_skips_reserved_and_advances (line 306) | def test_topic_find_in_item_for_topic_skips_reserved_and_advances(self):
    method test_topic_find_in_item_for_topic_after_missing_excludes_only_id (line 318) | def test_topic_find_in_item_for_topic_after_missing_excludes_only_id(s...
    method test_topic_find_in_item_for_topic_blank_item_id_returns_none (line 326) | def test_topic_find_in_item_for_topic_blank_item_id_returns_none(self):
    method test_topic_find_not_started_in_project_excludes_item_and_reserved (line 332) | def test_topic_find_not_started_in_project_excludes_item_and_reserved(...
    method test_topic_find_not_started_in_project_blank_slug_none (line 346) | def test_topic_find_not_started_in_project_blank_slug_none(self):
    method test_topic_order_unstarted_first_prefers_not_started (line 351) | def test_topic_order_unstarted_first_prefers_not_started(self):
    method test_topic_find_not_started_in_project_without_exclude_includes_same_item (line 363) | def test_topic_find_not_started_in_project_without_exclude_includes_sa...
  class NextTranscribableTopicMoreTests (line 377) | class NextTranscribableTopicMoreTests(CreateTestUsers, TestCase):
    method setUp (line 378) | def setUp(self):
    method test_new_transcribable_topic_excludes_reserved_and_cached (line 387) | def test_new_transcribable_topic_excludes_reserved_and_cached(self):
    method test_find_and_order_potential_transcribable_topic_assets_ordering (line 411) | def test_find_and_order_potential_transcribable_topic_assets_ordering(...
    method test_find_invalid_next_transcribable_topic_assets_reserved_and_status (line 456) | def test_find_invalid_next_transcribable_topic_assets_reserved_and_sta...
    method test_cache_same_item_is_ignored_then_manual_selects_topic (line 499) | def test_cache_same_item_is_ignored_then_manual_selects_topic(self, mo...
    method test_cache_excludes_original_pk_and_chooses_next_topic (line 535) | def test_cache_excludes_original_pk_and_chooses_next_topic(self, mock_...
    method test_same_item_inprogress_selected_when_no_not_started_topic (line 563) | def test_same_item_inprogress_selected_when_no_not_started_topic(self):
    method test_next_transcribable_topic_none_anywhere_returns_none_no_spawn (line 575) | def test_next_transcribable_topic_none_anywhere_returns_none_no_spawn(
    method test_item_gate_ignored_when_original_is_other_item_topic (line 595) | def test_item_gate_ignored_when_original_is_other_item_topic(self):
    method test_item_digit_string_missing_treats_as_no_after_topic (line 613) | def test_item_digit_string_missing_treats_as_no_after_topic(self):
    method test_manual_same_item_ip_when_no_ns_anywhere_topic (line 623) | def test_manual_same_item_ip_when_no_ns_anywhere_topic(self, mock_get_...
    method test_item_invalid_after_str_valueerror_branch_topic (line 645) | def test_item_invalid_after_str_valueerror_branch_topic(self):
    method test_manual_valid_after_excludes_original_and_picks_next_topic (line 655) | def test_manual_valid_after_excludes_original_and_picks_next_topic(
    method test_inprogress_fallback_spawns_and_uses_after_gate_topic (line 682) | def test_inprogress_fallback_spawns_and_uses_after_gate_topic(self, mo...
    method test_inprogress_fallback_with_digit_string_original_id (line 707) | def test_inprogress_fallback_with_digit_string_original_id(self, mock_...
    method test_inprogress_fallback_spawns_and_returns_asset_topic (line 733) | def test_inprogress_fallback_spawns_and_returns_asset_topic(self, mock...
    method test_inprogress_fallback_with_digit_str_original_id_topic (line 764) | def test_inprogress_fallback_with_digit_str_original_id_topic(self, mo...
    method test_project_short_circuit_topic_excludes_current_item_via_item_filter (line 797) | def test_project_short_circuit_topic_excludes_current_item_via_item_fi...
    method test_inprogress_fallback_spawns_task_with_item_id_topic (line 822) | def test_inprogress_fallback_spawns_task_with_item_id_topic(self, mock...
    method test_manual_same_item_inprogress_triggers_spawn_task_topic (line 844) | def test_manual_same_item_inprogress_triggers_spawn_task_topic(self, m...
    method test_project_short_circuit_excludes_current_item_topic (line 869) | def test_project_short_circuit_excludes_current_item_topic(self, mock_...
    method test_manual_inprogress_fallback_triggers_spawn_task_topic (line 888) | def test_manual_inprogress_fallback_triggers_spawn_task_topic(self, mo...
    method test_inprogress_fallback_item_id_returns_none_when_no_candidates_topic (line 906) | def test_inprogress_fallback_item_id_returns_none_when_no_candidates_t...
    method test_inprogress_fallback_returns_asset_without_spawning (line 942) | def test_inprogress_fallback_returns_asset_without_spawning(self, mock...

FILE: concordia/tests/test_validators.py
  class TestValidators (line 10) | class TestValidators(TestCase):
    method test_DjangoPasswordsValidator (line 11) | def test_DjangoPasswordsValidator(self):
  class ComplexityValidatorTests (line 46) | class ComplexityValidatorTests(TestCase):
    method assertValid (line 47) | def assertValid(self, validator, string):
    method assertInvalid (line 53) | def assertInvalid(self, validator, string):
    method make_validator (line 56) | def make_validator(self, **complexities):
    method test_empty_validator (line 59) | def test_empty_validator(self):
    method test_minimum_uppercase_count (line 63) | def test_minimum_uppercase_count(self):
    method test_minimum_lowercase_count (line 79) | def test_minimum_lowercase_count(self):
    method test_minimum_letter_count (line 95) | def test_minimum_letter_count(self):
    method test_minimum_digit_count (line 111) | def test_minimum_digit_count(self):
    method test_minimum_punctuation_count (line 126) | def test_minimum_punctuation_count(self):
    method test_minimum_nonascii_count (line 150) | def test_minimum_nonascii_count(self):
    method test_minimum_words_count (line 170) | def test_minimum_words_count(self):

FILE: concordia/tests/test_view_decorators.py
  class TestNextAssetRate (line 10) | class TestNextAssetRate(TestCase):
    method setUp (line 11) | def setUp(self):
    method test_authenticated_user_returns_none (line 14) | def test_authenticated_user_returns_none(self):
    method test_anonymous_user_valid_rate (line 21) | def test_anonymous_user_valid_rate(self, mock_validate_rate, mock_conf...
    method test_anonymous_user_invalid_rate_falls_back (line 31) | def test_anonymous_user_invalid_rate_falls_back(
    method test_anonymous_user_missing_value_falls_back (line 42) | def test_anonymous_user_missing_value_falls_back(self, mock_config_val...

FILE: concordia/tests/test_views.py
  function setup_view (line 52) | def setup_view(view, request, user=None, *args, **kwargs):
  class AccountProfileViewTests (line 64) | class AccountProfileViewTests(CreateTestUsers, TestCase):
    method test_get_queryset (line 69) | def test_get_queryset(self):
  class CompletedCampaignListViewTests (line 83) | class CompletedCampaignListViewTests(TestCase):
    method setUp (line 88) | def setUp(self):
    method test_get_all_campaigns (line 107) | def test_get_all_campaigns(self):
    method test_queryset (line 132) | def test_queryset(self):
    method test_context_data (line 152) | def test_context_data(self):
    method test_research_centers (line 175) | def test_research_centers(self):
  class ConcordiaViewTests (line 201) | class ConcordiaViewTests(CreateTestUsers, JSONAssertMixin, TestCase):
    method setUp (line 206) | def setUp(self):
    method tearDown (line 210) | def tearDown(self):
    method test_ratelimit_view (line 214) | def test_ratelimit_view(self):
    method test_campaign_topic_list_view (line 225) | def test_campaign_topic_list_view(self):
    method test_campaign_list_view (line 255) | def test_campaign_list_view(self):
    method test_campaign_detail_view (line 273) | def test_campaign_detail_view(self):
    method test_campaign_unicode_slug (line 353) | def test_campaign_unicode_slug(self):
    method test_concordiaCampaignView_get_page2 (line 364) | def test_concordiaCampaignView_get_page2(self):
    method test_empty_item_detail_view (line 379) | def test_empty_item_detail_view(self):
    method test_item_detail_view (line 402) | def test_item_detail_view(self):
    method test_asset_unicode_slug (line 487) | def test_asset_unicode_slug(self):
    method test_asset_detail_view (line 498) | def test_asset_detail_view(self):
    method test_generate_ocr_transcription (line 605) | def test_generate_ocr_transcription(self, mock):
    method test_project_detail_view (line 654) | def test_project_detail_view(self):
    method test_project_unicode_slug (line 707) | def test_project_unicode_slug(self):
    method test_campaign_report (line 718) | def test_campaign_report(self):
  class UserCacheControlTest (line 772) | class UserCacheControlTest(CreateTestUsers, TestCase):
    method setUp (line 777) | def setUp(self):
    method test_vary_on_cookie (line 781) | def test_vary_on_cookie(self):
  class FilteredCampaignDetailViewTests (line 792) | class FilteredCampaignDetailViewTests(CreateTestUsers, TestCase):
    method test_get_context_data (line 793) | def test_get_context_data(self):
  class FilteredProjectDetailViewTests (line 809) | class FilteredProjectDetailViewTests(CreateTestUsers, TestCase):
    method setUp (line 810) | def setUp(self):
    method test_get_queryset (line 819) | def test_get_queryset(self):
    method test_get_context_data (line 838) | def test_get_context_data(self):
    method tearDown (line 842) | def tearDown(self):
  class FilteredItemDetailViewTests (line 846) | class FilteredItemDetailViewTests(CreateTestUsers, TestCase):
    method setUp (line 847) | def setUp(self):
    method test_get_queryset (line 857) | def test_get_queryset(self):
    method test_get_context_data (line 872) | def test_get_context_data(self):
    method tearDown (line 876) | def tearDown(self):
  class RateLimitTests (line 880) | class RateLimitTests(CreateTestUsers, TestCase):
    method setUp (line 881) | def setUp(self):
    method test_registration_rate (line 885) | def test_registration_rate(self):
    method test_ratelimit_view (line 892) | def test_ratelimit_view(self):
    method test_reserve_rate (line 899) | def test_reserve_rate(self):
  class LoginTests (line 909) | class LoginTests(TestCase, CreateTestUsers):
    method setUp (line 910) | def setUp(self):
    method test_ConcordiaLoginView (line 913) | def test_ConcordiaLoginView(self):
  class TranscriptionViewTests (line 942) | class TranscriptionViewTests(CreateTestUsers, TestCase):
    method setUp (line 943) | def setUp(self):
    method test_rollback_transcription (line 946) | def test_rollback_transcription(self):
    method test_rollforward_transcription (line 1008) | def test_rollforward_transcription(self):
    method tearDown (line 1070) | def tearDown(self):
  class VisualizationDataViewTests (line 1081) | class VisualizationDataViewTests(TestCase):
    method setUp (line 1082) | def setUp(self):
    method test_get_missing_data_returns_404 (line 1089) | def test_get_missing_data_returns_404(self):
    method test_get_existing_data_returns_200_and_json (line 1101) | def test_get_existing_data_returns_200_and_json(self):

FILE: concordia/tests/test_views_asset_reservation.py
  class AssetReservationViewTests (line 36) | class AssetReservationViewTests(CreateTestUsers, JSONAssertMixin, Transa...
    method test_asset_reservation (line 37) | def test_asset_reservation(self):
    method test_asset_reservation_anonymously (line 45) | def test_asset_reservation_anonymously(self):
    method _asset_reservation_test_payload (line 53) | def _asset_reservation_test_payload(self, user_id, anonymous=False):
    method test_asset_reservation_competition (line 114) | def test_asset_reservation_competition(self):
    method test_asset_reservation_expiration (line 141) | def test_asset_reservation_expiration(self):
    method test_asset_reservation_tombstone (line 176) | def test_asset_reservation_tombstone(self):
    method test_asset_reservation_tombstone_expiration (line 243) | def test_asset_reservation_tombstone_expiration(self):
    method tearDown (line 297) | def tearDown(self):

FILE: concordia/tests/test_views_redirect_next_reviewable.py
  class NextReviewableRedirectViewTests (line 31) | class NextReviewableRedirectViewTests(
    method test_find_next_reviewable_no_campaign (line 34) | def test_find_next_reviewable_no_campaign(self):
    method test_find_next_reviewable_campaign (line 99) | def test_find_next_reviewable_campaign(self):
    method test_find_next_reviewable_topic (line 137) | def test_find_next_reviewable_topic(self):
    method test_find_next_reviewable_unlisted_campaign (line 176) | def test_find_next_reviewable_unlisted_campaign(self):
    method tearDown (line 216) | def tearDown(self):

FILE: concordia/tests/test_views_redirect_next_transcribable.py
  class NextTranscribableRedirectViewTests (line 31) | class NextTranscribableRedirectViewTests(
    method test_find_next_transcribable_no_campaign (line 34) | def test_find_next_transcribable_no_campaign(self):
    method test_find_next_transcribable_campaign (line 73) | def test_find_next_transcribable_campaign(self):
    method test_find_next_transcribable_topic (line 97) | def test_find_next_transcribable_topic(self):
    method test_find_next_transcribable_unlisted_campaign (line 122) | def test_find_next_transcribable_unlisted_campaign(self):
    method test_find_next_transcribable_single_asset (line 152) | def test_find_next_transcribable_single_asset(self):
    method test_find_next_transcribable_in_singleton_campaign (line 165) | def test_find_next_transcribable_in_singleton_campaign(self):
    method test_find_next_transcribable_project_redirect (line 178) | def test_find_next_transcribable_project_redirect(self):
    method test_find_next_transcribable_hierarchy (line 196) | def test_find_next_transcribable_hierarchy(self):
    method tearDown (line 288) | def tearDown(self):

FILE: concordia/tests/test_views_tags.py
  class TagSubmissionViewTests (line 27) | class TagSubmissionViewTests(CreateTestUsers, JSONAssertMixin, Transacti...
    method test_anonymous_tag_submission (line 28) | def test_anonymous_tag_submission(self):
    method test_tag_submission (line 36) | def test_tag_submission(self):
    method test_invalid_tag_submission (line 54) | def test_invalid_tag_submission(self):
    method test_tag_submission_with_diacritics (line 70) | def test_tag_submission_with_diacritics(self):
    method test_tag_submission_with_multiple_users (line 88) | def test_tag_submission_with_multiple_users(self):
    method test_duplicate_tag_submission (line 105) | def test_duplicate_tag_submission(self):
    method test_tag_deletion (line 135) | def test_tag_deletion(self):
    method test_tag_deletion_with_multiple_users (line 158) | def test_tag_deletion_with_multiple_users(self):
    method tearDown (line 205) | def tearDown(self):

FILE: concordia/tests/test_views_topics.py
  class TopicDetailViewTests (line 26) | class TopicDetailViewTests(CreateTestUsers, JSONAssertMixin, TestCase):
    method setUp (line 31) | def setUp(self):
    method tearDown (line 35) | def tearDown(self):
    method test_topic_detail_basic (line 39) | def test_topic_detail_basic(self):
    method test_unlisted_topic_detail_view (line 48) | def test_unlisted_topic_detail_view(self):
    method test_topic_detail_with_status_sets_querystring (line 61) | def test_topic_detail_with_status_sets_querystring(self):
    method test_url_filter_links_without_sublevel_querystring (line 81) | def test_url_filter_links_without_sublevel_querystring(self):
    method test_sublevel_querystring_only_keeps_transcription_status (line 124) | def test_sublevel_querystring_only_keeps_transcription_status(self):
    method test_with_status_and_no_assets_excludes_projects (line 140) | def test_with_status_and_no_assets_excludes_projects(self):
    method test_with_status_and_assets_uses_sublevel_and_overrides_url_filter (line 188) | def test_with_status_and_assets_uses_sublevel_and_overrides_url_filter...
    method test_with_status_and_assets_includes_matching_url_filter (line 246) | def test_with_status_and_assets_includes_matching_url_filter(self):
    method test_topic_detail_with_invalid_status_ignores_filter (line 307) | def test_topic_detail_with_invalid_status_ignores_filter(self):
    method test_url_filter_empty_string_treated_as_missing (line 360) | def test_url_filter_empty_string_treated_as_missing(self):

FILE: concordia/tests/test_views_transcription_review.py
  class ReviewTranscriptionViewTests (line 30) | class ReviewTranscriptionViewTests(
    method test_transcription_review (line 33) | def test_transcription_review(self):
    method test_transcription_review_rate_limit (line 63) | def test_transcription_review_rate_limit(self):
    method test_transcription_review_rate_limit_superuser (line 143) | def test_transcription_review_rate_limit_superuser(self):
    method test_transcription_review_asset_status_updates (line 223) | def test_transcription_review_asset_status_updates(self):
    method test_transcription_disallow_self_review (line 299) | def test_transcription_disallow_self_review(self):
    method test_transcription_allow_self_reject (line 315) | def test_transcription_allow_self_reject(self):
    method test_transcription_double_review (line 334) | def test_transcription_double_review(self):
    method tearDown (line 357) | def tearDown(self):

FILE: concordia/tests/test_views_transcription_save.py
  class SaveTranscriptionViewTests (line 27) | class SaveTranscriptionViewTests(CreateTestUsers, JSONAssertMixin, Trans...
    method setUp (line 28) | def setUp(self):
    method test_turnstile_validation_fails (line 31) | def test_turnstile_validation_fails(self):
    method test_initial_save_success (line 44) | def test_initial_save_success(self):
    method test_duplicate_without_supersedes_conflict (line 55) | def test_duplicate_without_supersedes_conflict(self):
    method test_save_with_url_error (line 72) | def test_save_with_url_error(self):
    method test_unacceptable_characters_are_removed_on_save (line 91) | def test_unacceptable_characters_are_removed_on_save(self):
    method test_unacceptable_characters_are_removed_when_superseding (line 105) | def test_unacceptable_characters_are_removed_when_superseding(self):
    method test_save_with_supersedes_success (line 126) | def test_save_with_supersedes_success(self):
    method test_supersedes_sets_ocr_originated_when_previous_was_ocr_originated (line 145) | def test_supersedes_sets_ocr_originated_when_previous_was_ocr_originat...
    method test_supersede_already_superseded_conflict (line 170) | def test_supersede_already_superseded_conflict(self):
    method test_supersede_nonexistent_returns_400 (line 200) | def test_supersede_nonexistent_returns_400(self):
    method test_supersede_invalid_pk_returns_400 (line 216) | def test_supersede_invalid_pk_returns_400(self):
    method test_logged_in_user_can_take_over_from_anonymous (line 232) | def test_logged_in_user_can_take_over_from_anonymous(self):
    method tearDown (line 254) | def tearDown(self):

FILE: concordia/tests/test_views_transcription_submit.py
  class SubmitTranscriptionViewTests (line 27) | class SubmitTranscriptionViewTests(
    method test_anonymous_transcription_submission (line 30) | def test_anonymous_transcription_submission(self):
    method test_transcription_submission (line 60) | def test_transcription_submission(self):
    method test_stale_transcription_submission (line 87) | def test_stale_transcription_submission(self):
    method tearDown (line 107) | def tearDown(self):

FILE: concordia/tests/test_views_utils.py
  class GetPagesTests (line 30) | class GetPagesTests(CreateTestUsers, TestCase):
    method setUp (line 31) | def setUp(self):
    method _request (line 56) | def _request(self, params: dict[str, str]):
    method _touch_transcription_times (line 61) | def _touch_transcription_times(
    method test_activity_filters_transcribed_vs_reviewed_vs_default (line 77) | def test_activity_filters_transcribed_vs_reviewed_vs_default(self):
    method test_status_filter_exclusions (line 117) | def test_status_filter_exclusions(self):
    method test_date_range_and_single_day_filters_and_ordering (line 140) | def test_date_range_and_single_day_filters_and_ordering(self):
    method test_campaign_filter_and_six_month_cutoff (line 193) | def test_campaign_filter_and_six_month_cutoff(self):
    method test_status_filter_includes_completed_when_requested (line 234) | def test_status_filter_includes_completed_when_requested(self):
    method test_status_filter_includes_in_progress_and_excludes_submitted_not_requested (line 263) | def test_status_filter_includes_in_progress_and_excludes_submitted_not...
  class CalculateAssetStatsTests (line 287) | class CalculateAssetStatsTests(CreateTestUsers, TestCase):
    method setUp (line 288) | def setUp(self):
    method test_counts_percents_and_contributors_remove_none_branch (line 296) | def test_counts_percents_and_contributors_remove_none_branch(self):
    method test_contributors_keyerror_branch_and_cap_99 (line 360) | def test_contributors_keyerror_branch_and_cap_99(self):
  class AnnotateChildrenProgressStatsTests (line 405) | class AnnotateChildrenProgressStatsTests(TestCase):
    class Obj (line 406) | class Obj:
    method test_progress_stats_with_capping_and_lowest_status (line 409) | def test_progress_stats_with_capping_and_lowest_status(self):
    method test_progress_stats_zero_total (line 431) | def test_progress_stats_zero_total(self):
  class _BaseView (line 448) | class _BaseView:
    method get_context_data (line 453) | def get_context_data(self, **kwargs):
  class DummyTemplateView (line 457) | class DummyTemplateView(AnonymousUserValidationCheckMixin, _BaseView):
  class AnonymousUserValidationCheckMixinTests (line 466) | class AnonymousUserValidationCheckMixinTests(CreateTestUsers, TestCase):
    method setUp (line 467) | def setUp(self):
    method _attach_session (line 471) | def _attach_session(self, request):
    method test_unauthenticated_requires_validation_when_stale (line 477) | def test_unauthenticated_requires_validation_when_stale(self):
    method test_unauthenticated_recent_validation_is_not_required (line 489) | def test_unauthenticated_recent_validation_is_not_required(self):
    method test_authenticated_never_requires_validation (line 500) | def test_authenticated_never_requires_validation(self):

FILE: concordia/tests/test_widgets.py
  class TestWidgets (line 7) | class TestWidgets(TestCase):
    method test_EmailWidget (line 8) | def test_EmailWidget(self):
    method test_TurnstileWidget (line 30) | def test_TurnstileWidget(self):

FILE: concordia/tests/utils.py
  function ensure_slug (line 33) | def ensure_slug(original_function):
  function create_campaign (line 47) | def create_campaign(
  function create_simple_page (line 74) | def create_simple_page(*, do_save=True, **kwargs):
  function create_site_report (line 81) | def create_site_report(*, do_save=True, **kwargs):
  function create_topic (line 89) | def create_topic(
  function create_project (line 123) | def create_project(
  function create_item (line 145) | def create_item(
  function create_asset (line 173) | def create_asset(
  function create_transcription (line 202) | def create_transcription(*, asset=None, user=None, do_save=True, **kwargs):
  function create_tag (line 214) | def create_tag(*, value="tag-value", do_save=True, **kwargs):
  function create_tag_collection (line 222) | def create_tag_collection(*, tag=None, asset=None, user=None, **kwargs):
  function create_banner (line 238) | def create_banner(*, slug="Test Banner", do_save=True, **kwargs):
  function create_card (line 245) | def create_card(*, title="Test Card", do_save=True, **kwargs):
  function create_card_family (line 252) | def create_card_family(*, slug="test-card-family", do_save=True, **kwargs):
  function create_carousel_slide (line 259) | def create_carousel_slide(*, headline="Test Headline", do_save=True, **k...
  function create_guide (line 266) | def create_guide(*, do_save=True, **kwargs):
  function create_helpful_link (line 273) | def create_helpful_link(*, title="Test Helpful Link", do_save=True, **kw...
  function create_concordia_file (line 280) | def create_concordia_file(
  function create_user_profile_activity (line 289) | def create_user_profile_activity(
  function create_campaign_retirement_progress (line 306) | def create_campaign_retirement_progress(
  function create_research_center (line 320) | def create_research_center(*, title="Test Research Center", do_save=True...
  class JSONAssertMixin (line 327) | class JSONAssertMixin(object):
    method assertValidJSON (line 328) | def assertValidJSON(self, response, expected_status=200):
  class CreateTestUsers (line 343) | class CreateTestUsers(object):
    method login_user (line 344) | def login_user(self, username="tester", **kwargs):
    method logout_user (line 353) | def logout_user(self):
    method create_user (line 358) | def create_user(cls, username, is_active=True, **kwargs):
    method create_test_user (line 373) | def create_test_user(cls, username="testuser", **kwargs):
    method create_inactive_user (line 380) | def create_inactive_user(cls, username="testinactiveuser", **kwargs):
    method create_staff_user (line 387) | def create_staff_user(cls, username="teststaffuser", **kwargs):
    method create_super_user (line 394) | def create_super_user(cls, username="testsuperuser", **kwargs):
  class CacheControlAssertions (line 403) | class CacheControlAssertions(object):
    method assertUncacheable (line 404) | def assertUncacheable(self, response):
    method assertCachePrivate (line 409) | def assertCachePrivate(self, response):
  class StreamingTestMixin (line 414) | class StreamingTestMixin(object):
    method get_streaming_content (line 415) | def get_streaming_content(self, response):

FILE: concordia/turnstile/context_processors.py
  function turnstile_default_settings (line 7) | def turnstile_default_settings(request: "HttpRequest") -> "Dict[str, Any]":

FILE: concordia/turnstile/fields.py
  class TurnstileField (line 24) | class TurnstileField(forms.Field):
    method __init__ (line 78) | def __init__(self, **kwargs: Any) -> None:
    method widget_attrs (line 110) | def widget_attrs(self, widget: forms.Widget) -> dict[str, Any]:
    method validate (line 127) | def validate(self, value: str | None) -> None:

FILE: concordia/turnstile/widgets.py
  class TurnstileWidget (line 11) | class TurnstileWidget(forms.Widget):
    method __init__ (line 34) | def __init__(self, *args, **kwargs) -> None:
    method value_from_datadict (line 48) | def value_from_datadict(
    method build_attrs (line 71) | def build_attrs(
    method get_context (line 94) | def get_context(

FILE: concordia/utils/__init__.py
  function get_anonymous_user (line 18) | def get_anonymous_user():
  function request_accepts_json (line 31) | def request_accepts_json(request):
  function get_or_create_reservation_token (line 37) | def get_or_create_reservation_token(request):
  function get_image_urls_from_asset (line 65) | def get_image_urls_from_asset(asset):

FILE: concordia/utils/celery.py
  function get_registered_task (line 6) | def get_registered_task(name: str) -> Task:

FILE: concordia/utils/next_asset/__init__.py
  function remove_next_asset_objects (line 69) | def remove_next_asset_objects(asset_id):

FILE: concordia/utils/next_asset/reviewable/campaign.py
  function _reserved_asset_ids_subq (line 14) | def _reserved_asset_ids_subq(
  function _eligible_reviewable_base_qs (line 38) | def _eligible_reviewable_base_qs(
  function _next_seq_after (line 70) | def _next_seq_after(pk: int | None) -> int | None:
  function _find_reviewable_in_item (line 95) | def _find_reviewable_in_item(
  function _find_reviewable_in_project (line 189) | def _find_reviewable_in_project(
  function find_new_reviewable_campaign_assets (line 257) | def find_new_reviewable_campaign_assets(
  function find_next_reviewable_campaign_assets (line 301) | def find_next_reviewable_campaign_assets(
  function find_reviewable_campaign_asset (line 326) | def find_reviewable_campaign_asset(
  function find_and_order_potential_reviewable_campaign_assets (line 398) | def find_and_order_potential_reviewable_campaign_assets(
  function find_next_reviewable_campaign_asset (line 465) | def find_next_reviewable_campaign_asset(
  function find_invalid_next_reviewable_campaign_assets (line 593) | def find_invalid_next_reviewable_campaign_assets(

FILE: concordia/utils/next_asset/reviewable/topic.py
  function _reserved_asset_ids_subq (line 14) | def _reserved_asset_ids_subq() -> "QuerySet[Dict[str, int]]":
  function _eligible_reviewable_base_qs (line 31) | def _eligible_reviewable_base_qs(
  function _next_seq_after (line 63) | def _next_seq_after(pk: int | None) -> int | None:
  function _find_reviewable_in_item (line 88) | def _find_reviewable_in_item(
  function _find_reviewable_in_project (line 182) | def _find_reviewable_in_project(
  function find_new_reviewable_topic_assets (line 251) | def find_new_reviewable_topic_assets(
  function find_next_reviewable_topic_assets (line 298) | def find_next_reviewable_topic_assets(
  function find_reviewable_topic_asset (line 323) | def find_reviewable_topic_asset(
  function find_and_order_potential_reviewable_topic_assets (line 396) | def find_and_order_potential_reviewable_topic_assets(
  function find_next_reviewable_topic_asset (line 464) | def find_next_reviewable_topic_asset(
  function find_invalid_next_reviewable_topic_assets (line 593) | def find_invalid_next_reviewable_topic_assets(

FILE: concordia/utils/next_asset/transcribable/campaign.py
  function _reserved_asset_ids_subq (line 13) | def _reserved_asset_ids_subq(
  function _eligible_transcribable_base_qs (line 37) | def _eligible_transcribable_base_qs(
  function _next_seq_after (line 66) | def _next_seq_after(pk: int | None) -> int | None:
  function _order_unstarted_first (line 90) | def _order_unstarted_first(
  function _find_transcribable_in_item (line 117) | def _find_transcribable_in_item(
  function _find_transcribable_not_started_in_project (line 184) | def _find_transcribable_not_started_in_project(
  function find_new_transcribable_campaign_assets (line 227) | def find_new_transcribable_campaign_assets(
  function find_next_transcribable_campaign_assets (line 268) | def find_next_transcribable_campaign_assets(
  function find_transcribable_campaign_asset (line 290) | def find_transcribable_campaign_asset(
  function find_and_order_potential_transcribable_campaign_assets (line 357) | def find_and_order_potential_transcribable_campaign_assets(
  function find_next_transcribable_campaign_asset (line 422) | def find_next_transcribable_campaign_asset(
  function find_invalid_next_transcribable_campaign_assets (line 638) | def find_invalid_next_transcribable_campaign_assets(

FILE: concordia/utils/next_asset/transcribable/topic.py
  function _reserved_asset_ids_subq (line 13) | def _reserved_asset_ids_subq() -> "QuerySet[Dict[str, int]]":
  function _eligible_transcribable_base_qs (line 30) | def _eligible_transcribable_base_qs(
  function _next_seq_after (line 59) | def _next_seq_after(pk: int | None) -> int | None:
  function _order_unstarted_first (line 83) | def _order_unstarted_first(
  function _find_transcribable_in_item_for_topic (line 109) | def _find_transcribable_in_item_for_topic(
  function _find_transcribable_not_started_in_project_for_topic (line 177) | def _find_transcribable_not_started_in_project_for_topic(
  function find_new_transcribable_topic_assets (line 221) | def find_new_transcribable_topic_assets(
  function find_next_transcribable_topic_assets (line 265) | def find_next_transcribable_topic_assets(
  function find_transcrib
Condensed preview — 673 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (3,374K chars).
[
  {
    "path": ".cfnlintrc.yaml",
    "chars": 263,
    "preview": "# The W2001 check is used to ignore the featurebranch.yaml DataLoadStackName parameter in the nested\n# stack fargate-fea"
  },
  {
    "path": ".dockerignore",
    "chars": 26,
    "preview": "node_modules\nstatic-files\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 708,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\n---\n\n**What behavior did you observe? Please describe the"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 690,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\n---\n\n**User story/persona**\nAs {a user}, I want to {ac"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 891,
    "preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
  },
  {
    "path": ".github/workflows/black.yml",
    "chars": 585,
    "preview": "name: Lint\n\non:\n    workflow_dispatch:\n    pull_request:\n        branches: [main, 'feature-*', release]\n        paths-ig"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 1696,
    "preview": "name: 'Build'\n\non:\n    workflow_dispatch:\n    pull_request:\n        branches: [main, 'feature-*', release]\n        paths"
  },
  {
    "path": ".github/workflows/codeql.yml",
    "chars": 2216,
    "preview": "name: 'CodeQL Advanced'\n\non:\n    workflow_dispatch:\n    push:\n        branches: [main, 'feature-*']\n    pull_request:\n  "
  },
  {
    "path": ".github/workflows/db_ops.yml",
    "chars": 3360,
    "preview": "name: DB Operations Multi-Repo Pipeline\n\non:\n    workflow_dispatch:\n        inputs:\n            action_type:\n           "
  },
  {
    "path": ".github/workflows/dev-main-deploy.yml",
    "chars": 4296,
    "preview": "name: 'Deploy to dev'\n\non:\n    workflow_dispatch:\n    push:\n        branches: [main]\n        paths-ignore:\n            -"
  },
  {
    "path": ".github/workflows/feature-branch-deploy.yml",
    "chars": 4245,
    "preview": "name: 'Deploy feature branch to test'\n\non:\n    workflow_dispatch:\n    push:\n        branches: ['feature-*']\n        path"
  },
  {
    "path": ".github/workflows/pip-audit.yml",
    "chars": 882,
    "preview": "name: pip-audit\n\non:\n    workflow_dispatch:\n    pull_request:\n        branches: [main, release]\n        paths-ignore:\n  "
  },
  {
    "path": ".github/workflows/prod-deploy.yml",
    "chars": 2299,
    "preview": "name: 'Deploy to production'\n\non:\n    workflow_dispatch:\n\nenv:\n    AWS_REGION: us-east-1\n\npermissions:\n    id-token: wri"
  },
  {
    "path": ".github/workflows/renew_coverage.yml",
    "chars": 838,
    "preview": "name: Renew Coverage Cache\n\non:\n    schedule:\n        - cron: '0 0 */5 * *' # Runs every 5 days at midnight UTC\n    work"
  },
  {
    "path": ".github/workflows/stage-hotfix-rel-deploy.yml",
    "chars": 3890,
    "preview": "name: 'Deploy hotfix to stage'\n\non:\n    workflow_dispatch:\n\nenv:\n    AWS_REGION: us-east-1\n\npermissions:\n    id-token: w"
  },
  {
    "path": ".github/workflows/stage-image-refresh.yml",
    "chars": 5451,
    "preview": "name: 'Deploy image refresh to stage'\n\non:\n    workflow_dispatch:\n\nenv:\n    AWS_REGION: us-east-1\n\npermissions:\n    id-t"
  },
  {
    "path": ".github/workflows/stage-release-deploy.yml",
    "chars": 4252,
    "preview": "name: 'Deploy release to stage'\n\non:\n    workflow_dispatch:\n    push:\n        branches: [release]\n        paths-ignore:\n"
  },
  {
    "path": ".github/workflows/test-main-deploy.yml",
    "chars": 2281,
    "preview": "name: 'Deploy to test'\n\non:\n    workflow_dispatch:\n\nenv:\n    AWS_REGION: us-east-1\n\npermissions:\n    id-token: write\n   "
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 9958,
    "preview": "name: Test\n\non:\n    workflow_dispatch:\n    pull_request:\n        branches: [main, 'feature-*', release]\n        paths-ig"
  },
  {
    "path": ".gitignore",
    "chars": 489,
    "preview": "node_modules/\nbin/\ntarget/\nlocal/\nbuild/\n.project\n.classpath\n.settings/\n*.pyc\nbuildstatus.log\ndeploystatus.log\n.metadata"
  },
  {
    "path": "Dockerfile",
    "chars": 5983,
    "preview": "# Base runtime: Debian 12 (bookworm) slim + Python 3.12.\nFROM python:3.12-slim-bookworm\n\n# Major Node.js version to inst"
  },
  {
    "path": "LICENSE.md",
    "chars": 1327,
    "preview": "As a work of the United States Government, this project is in the\npublic domain within the United States.\n\nAdditionally,"
  },
  {
    "path": "Loadtesting.md",
    "chars": 10145,
    "preview": "# Load Testing Mode\n\nThis document describes the current (incomplete but runnable) \"load testing mode\"\nimplementation an"
  },
  {
    "path": "MANIFEST.in",
    "chars": 97,
    "preview": "include README.md\ninclude MANIFEST.in\nrecursive-include concordia *\nrecursive-include tests *.py\n"
  },
  {
    "path": "Makefile",
    "chars": 581,
    "preview": ".PHONY: allup firstup adminuser devup down clean\n\nfirstup:\n\tdocker-compose -f docker-compose.yml up -d\n\tadminuser\n\nadmin"
  },
  {
    "path": "Pipfile",
    "chars": 1748,
    "preview": "[[source]]\nurl = \"https://pypi.org/simple\"\nverify_ssl = true\nname = \"pypi\"\n\n[packages]\ngunicorn = \"==23.0.0\"\ncelery = { "
  },
  {
    "path": "README.md",
    "chars": 4648,
    "preview": "[![Lint](https://github.com/LibraryOfCongress/concordia/actions/workflows/black.yml/badge.svg)](https://github.com/Libra"
  },
  {
    "path": "build_containers.sh",
    "chars": 2435,
    "preview": "#!/bin/bash\n\nset -eu -o pipefail\n\nBUILD_ALL=${BUILD_ALL:=0}\nBUILD_NUMBER=${BUILD_NUMBER:=1}\nTAG=${TAG:-test}\nPUBLISH_CON"
  },
  {
    "path": "celerybeat/Dockerfile",
    "chars": 1688,
    "preview": "FROM python:3.12-slim-bookworm\n\n## Add the wait script to the image\nADD https://github.com/ufoscout/docker-compose-wait/"
  },
  {
    "path": "celerybeat/entrypoint.sh",
    "chars": 358,
    "preview": "#!/bin/bash\n\nset -e -u # Exit immediately for unhandled errors or undefined variables\n\nmkdir -p /app/logs\ntouch /app/log"
  },
  {
    "path": "cloudformation/LICENSE",
    "chars": 11355,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "cloudformation/NOTICE",
    "chars": 104,
    "preview": "ecs-refarch-cloudformation\nCopyright 2011-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.\n"
  },
  {
    "path": "cloudformation/README.md",
    "chars": 23326,
    "preview": "# Note Regarding Concordia Usage\n\nThis README, and set of CloudFormation templates, is based on the AWS sample templates"
  },
  {
    "path": "cloudformation/add_cloudflare_ips_to_sgs.py",
    "chars": 2159,
    "preview": "#!/usr/bin/env python3\n\"\"\"\nEnsure that every security group tagged with “AllowCloudFlareIngress” has\npermissions for eve"
  },
  {
    "path": "cloudformation/create_secrets.sh",
    "chars": 999,
    "preview": "#!/bin/bash\n\nset -eu\n\n# If you create a new set of secrets using a new ENV_NAME here,\n# then add the new ENV_NAME option"
  },
  {
    "path": "cloudformation/featurebranch.yaml",
    "chars": 3022,
    "preview": "---\nAWSTemplateFormatVersion: '2010-09-09'\nDescription: >\n    Deploy a feature branch to a subdomain of crowd-test.loc.g"
  },
  {
    "path": "cloudformation/infrastructure/bastion-hosts.yaml",
    "chars": 6267,
    "preview": "Description: This template deploys a bastion host in each of the public subnets.\n\nParameters:\n    EnvironmentName:\n     "
  },
  {
    "path": "cloudformation/infrastructure/data-load.yaml",
    "chars": 4856,
    "preview": "Description:\n    This template deploys a host in a private subnet and loads the most recent\n    database dump to the spe"
  },
  {
    "path": "cloudformation/infrastructure/elasticache-feature.yaml",
    "chars": 1162,
    "preview": "Description: >\n    This template deploys an elasticache cluster to the provided VPC and subnets\n\nParameters:\n    Environ"
  },
  {
    "path": "cloudformation/infrastructure/elasticache.yaml",
    "chars": 1624,
    "preview": "Description: >\n    This template deploys an elasticache cluster to the provided VPC and subnets\n\nParameters:\n    Environ"
  },
  {
    "path": "cloudformation/infrastructure/elasticsearch.yaml",
    "chars": 1844,
    "preview": "Description: >\n    This template deploys a VPC-based ElasticSearch cluster.\n\nParameters:\n    EnvName:\n        Type: Stri"
  },
  {
    "path": "cloudformation/infrastructure/fargate-cluster.yaml",
    "chars": 17360,
    "preview": "Description: >\n    This template deploys a fargate cluster to the provided VPC and subnets\n\nParameters:\n    EnvironmentN"
  },
  {
    "path": "cloudformation/infrastructure/fargate-featurebranch.yaml",
    "chars": 10885,
    "preview": "Description: >\n    This template deploys a fargate cluster to the provided VPC and subnets\n\nParameters:\n    SecurityGrou"
  },
  {
    "path": "cloudformation/infrastructure/jenkins-server.yaml",
    "chars": 2356,
    "preview": "Description: This template deploys an Ubuntu jenkins server in the default VPC.\n\nResources:\n    Jenkins:\n        Type: A"
  },
  {
    "path": "cloudformation/infrastructure/network-acl.yaml",
    "chars": 5418,
    "preview": "Description: >\n    This template contains the security groups required by our entire stack.\n    We create them in a sepe"
  },
  {
    "path": "cloudformation/infrastructure/opensearch.yaml",
    "chars": 1813,
    "preview": "Description: >\n    This template deploys a VPC-based OpenSearch cluster.\n\nParameters:\n    EnvName:\n        Type: String\n"
  },
  {
    "path": "cloudformation/infrastructure/rds.yaml",
    "chars": 2249,
    "preview": "AWSTemplateFormatVersion: '2010-09-09'\nParameters:\n    DatabaseSecurityGroup:\n        Description: Sets the security gro"
  },
  {
    "path": "cloudformation/infrastructure/search-proxy-task.yaml",
    "chars": 8488,
    "preview": "Description: >\n    This template deploys an opensearch dashboard proxy server to the specified VPC\n\nParameters:\n    VpcI"
  },
  {
    "path": "cloudformation/infrastructure/security-groups.yaml",
    "chars": 6158,
    "preview": "Description: >\n    This template contains the security groups required by our entire stack.\n    We create them in a sepe"
  },
  {
    "path": "cloudformation/infrastructure/vpc.yaml",
    "chars": 10080,
    "preview": "Description: >\n    This template deploys a VPC, with a pair of public and private subnets spread\n    across two Availabi"
  },
  {
    "path": "cloudformation/master.yaml",
    "chars": 9365,
    "preview": "---\nAWSTemplateFormatVersion: '2010-09-09'\nDescription: >\n\n    This template deploys a VPC, with a pair of public and pr"
  },
  {
    "path": "cloudformation/stack_drift.sh",
    "chars": 2876,
    "preview": "#!/bin/bash\n\nset -eu -o pipefail\n\nSTACK_NAME=$1\nif [[ -z \"${STACK_NAME}\" ]]; then\n    echo \"STACK_NAME must be set prior"
  },
  {
    "path": "cloudformation/sync_templates.sh",
    "chars": 58,
    "preview": "#!/bin/bash\n\nset -eu\n\naws s3 sync . s3://crowd-deployment\n"
  },
  {
    "path": "cloudformation/tests/validate-templates.sh",
    "chars": 583,
    "preview": "#!/bin/bash\nERROR_COUNT=0;\n\necho \"Validating AWS CloudFormation templates...\"\n\n# Loop through the YAML templates in this"
  },
  {
    "path": "concordia/__init__.py",
    "chars": 131,
    "preview": "from __future__ import absolute_import, unicode_literals\n\nfrom concordia.celery import app as celery_app\n\n__all__ = [\"ce"
  },
  {
    "path": "concordia/admin/__init__.py",
    "chars": 62268,
    "preview": "import io\nimport logging\nimport zipfile\nfrom typing import Any\n\nfrom django.contrib import admin, messages\nfrom django.c"
  },
  {
    "path": "concordia/admin/actions.py",
    "chars": 12050,
    "preview": "import uuid\nfrom logging import getLogger\n\nfrom django.contrib import admin, messages\nfrom django.db.models import Query"
  },
  {
    "path": "concordia/admin/filters.py",
    "chars": 13409,
    "preview": "from django.contrib import admin\nfrom django.db.models import Exists, OuterRef\nfrom django.utils.translation import gett"
  },
  {
    "path": "concordia/admin/forms.py",
    "chars": 11004,
    "preview": "import nh3\nfrom django import forms\nfrom django.core.cache import caches\nfrom tinymce.widgets import TinyMCE\n\nfrom ..mod"
  },
  {
    "path": "concordia/admin/utils.py",
    "chars": 3565,
    "preview": "from django.contrib.auth.models import User\nfrom django.db.models import Prefetch\nfrom django.utils.timezone import now\n"
  },
  {
    "path": "concordia/admin/views.py",
    "chars": 29920,
    "preview": "import logging\nimport re\nimport tempfile\nimport time\nfrom http import HTTPStatus\nfrom typing import Any\n\nfrom django.app"
  },
  {
    "path": "concordia/admin_site.py",
    "chars": 2222,
    "preview": "\"\"\"Admin site customizations for Concordia.\n\nProvides a subclass of Django's ``AdminSite`` that adds project-specific\nad"
  },
  {
    "path": "concordia/api/__init__.py",
    "chars": 26392,
    "preview": "\"\"\"\nExperimental API endpoints backing the React transcription page.\n\nStatus\n------\nThis module is in active development"
  },
  {
    "path": "concordia/api/schemas.py",
    "chars": 964,
    "preview": "from ninja import Schema\n\n\ndef to_camel(string: str) -> str:\n    \"\"\"\n    Convert a snake_case string to camelCase.\n\n    "
  },
  {
    "path": "concordia/api_views.py",
    "chars": 5153,
    "preview": "\"\"\"\nVery simple generic API views\n\nThese provide base classes for Django CBVs which behave differently when the URL\nends"
  },
  {
    "path": "concordia/apps.py",
    "chars": 542,
    "preview": "from django.apps.config import AppConfig\nfrom django.contrib.admin.apps import AdminConfig\nfrom django.contrib.staticfil"
  },
  {
    "path": "concordia/asgi.py",
    "chars": 207,
    "preview": "\"\"\"\nASGI entrypoint — see https://channels.readthedocs.io/en/latest/asgi.html\n\"\"\"\n\nimport django\nfrom channels.routing i"
  },
  {
    "path": "concordia/authentication_backends.py",
    "chars": 3012,
    "preview": "from typing import Any\n\nfrom django.contrib.auth import get_user_model\nfrom django.contrib.auth.backends import ModelBac"
  },
  {
    "path": "concordia/celery.py",
    "chars": 1732,
    "preview": "import importlib\nimport os\nimport pkgutil\n\nimport sentry_sdk\nfrom celery import Celery\nfrom sentry_sdk.integrations.cele"
  },
  {
    "path": "concordia/consumers.py",
    "chars": 779,
    "preview": "import time\n\nfrom channels.generic.websocket import AsyncJsonWebsocketConsumer\n\n\nclass AssetConsumer(AsyncJsonWebsocketC"
  },
  {
    "path": "concordia/context_processors.py",
    "chars": 3356,
    "preview": "from typing import Any, Dict\n\nfrom django.conf import settings\nfrom django.core.cache import cache\nfrom django.http impo"
  },
  {
    "path": "concordia/contextmanagers.py",
    "chars": 2012,
    "preview": "# Based on code from\n# https://docs.celeryq.dev/en/v5.5.0/tutorials/task-cookbook.html#ensuring-a-task-is-only-executed-"
  },
  {
    "path": "concordia/converters.py",
    "chars": 345,
    "preview": "from django.urls.converters import SlugConverter, StringConverter\n\n\nclass UnicodeSlugConverter(SlugConverter):\n    # Thi"
  },
  {
    "path": "concordia/decorators.py",
    "chars": 3529,
    "preview": "# Based on code from https://gist.github.com/dmwyatt/d09da3f03cbdcad217db35f5cf8a9f94\nimport hashlib\nimport logging\nfrom"
  },
  {
    "path": "concordia/documents.py",
    "chars": 7720,
    "preview": "# Contains OpenSearch documents for indexing models in the Concordia application.\nfrom django.contrib.auth.models import"
  },
  {
    "path": "concordia/exceptions.py",
    "chars": 315,
    "preview": "# Creating a specfic error for this, since our pre-commit\n# checks will not allow us to test for generic exceptions\nclas"
  },
  {
    "path": "concordia/forms.py",
    "chars": 7659,
    "preview": "from logging import getLogger\nfrom typing import Any, Iterator\n\nfrom django import forms\nfrom django.contrib.auth import"
  },
  {
    "path": "concordia/logging.py",
    "chars": 14141,
    "preview": "import warnings\nfrom types import MappingProxyType\nfrom typing import Any, Callable, Optional\n\nimport structlog\n\n\ndef ge"
  },
  {
    "path": "concordia/maintenance.py",
    "chars": 1987,
    "preview": "\"\"\"\nMaintenance-mode helpers for conditional frontend availability.\n\nThis module wraps ``maintenance_mode.http.need_main"
  },
  {
    "path": "concordia/management/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "concordia/management/commands/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "concordia/management/commands/calculate_difficulty_values.py",
    "chars": 1151,
    "preview": "\"\"\"\nManagement command to populate initial difficulty values.\n\nUsage:\n    python manage.py calculate_difficulty_values\n "
  },
  {
    "path": "concordia/management/commands/create_load_test_fixtures.py",
    "chars": 15571,
    "preview": "# ruff: noqa: ERA001 A003\n# bandit:skip-file\n\nimport json\nimport uuid\nfrom pathlib import Path\n\nfrom django.contrib.auth"
  },
  {
    "path": "concordia/management/commands/ensure_initial_site_configuration.py",
    "chars": 4456,
    "preview": "\"\"\"\nEnsure that basic site configuration has been applied.\n\nThis command is intended for automated scenarios: a fresh da"
  },
  {
    "path": "concordia/management/commands/import_site_reports.py",
    "chars": 3515,
    "preview": "\"\"\"\nImport CSV Site Report data into the database.\n\nThis command reads a CSV file, maps each row to `SiteReport` fields "
  },
  {
    "path": "concordia/management/commands/prepare_load_test_db.py",
    "chars": 7263,
    "preview": "# ruff: noqa: ERA001 A003\n# bandit:skip-file\n\nfrom contextlib import contextmanager\nfrom pathlib import Path\n\nfrom djang"
  },
  {
    "path": "concordia/management/commands/print_frontend_test_urls.py",
    "chars": 3506,
    "preview": "\"\"\"\nPrint a list of URLs (derived from local database content) suitable for\nfront-end testing.\n\nUsage:\n    python manage"
  },
  {
    "path": "concordia/middleware.py",
    "chars": 445,
    "preview": "from maintenance_mode.http import get_maintenance_response\nfrom maintenance_mode.middleware import (\n    MaintenanceMode"
  },
  {
    "path": "concordia/migrations/0001_initial.py",
    "chars": 1047,
    "preview": "# Generated by Django 2.0.4 on 2018-04-17 18:59\n\nimport django.db.models.deletion\nfrom django.conf import settings\nfrom "
  },
  {
    "path": "concordia/migrations/0001_squashed_0040_remove_campaign_is_active.py",
    "chars": 23231,
    "preview": "# Generated by Django 2.0.9 on 2018-10-03 20:04\n\nimport django.contrib.postgres.fields.jsonb\nimport django.core.validato"
  },
  {
    "path": "concordia/migrations/0002_auto_20181004_1848.py",
    "chars": 730,
    "preview": "# Generated by Django 2.0.9 on 2018-10-04 18:48\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "concordia/migrations/0003_auto_20181004_2103.py",
    "chars": 1545,
    "preview": "# Generated by Django 2.0.9 on 2018-10-04 21:03\n\nimport django.db.models.deletion\nfrom django.conf import settings\nfrom "
  },
  {
    "path": "concordia/migrations/0004_auto_20181010_1715.py",
    "chars": 2780,
    "preview": "# Generated by Django 2.0.9 on 2018-10-10 17:15\n\nimport django.db.models.deletion\nfrom django.conf import settings\nfrom "
  },
  {
    "path": "concordia/migrations/0005_campaign_short_description.py",
    "chars": 379,
    "preview": "# Generated by Django 2.0.9 on 2018-10-10 19:31\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "concordia/migrations/0006_campaignresource.py",
    "chars": 1093,
    "preview": "# Generated by Django 2.0.9 on 2018-10-10 20:19\n\nimport django.db.models.deletion\nfrom django.db import migrations, mode"
  },
  {
    "path": "concordia/migrations/0007_thumbnail_images.py",
    "chars": 779,
    "preview": "# Generated by Django 2.0.9 on 2018-10-10 18:20\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "concordia/migrations/0008_auto_20181015_1711.py",
    "chars": 521,
    "preview": "# Generated by Django 2.0.9 on 2018-10-15 17:11\n\nfrom django.db import migrations\n\n\nclass Migration(migrations.Migration"
  },
  {
    "path": "concordia/migrations/0009_project_description.py",
    "chars": 347,
    "preview": "# Generated by Django 2.0.9 on 2018-10-17 15:13\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "concordia/migrations/0010_auto_20181021_1659.py",
    "chars": 471,
    "preview": "# Generated by Django 2.0.9 on 2018-10-21 16:59\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "concordia/migrations/0010_auto_20181022_1530.py",
    "chars": 410,
    "preview": "# Generated by Django 2.0.9 on 2018-10-22 15:30\n\nfrom django.db import migrations\n\n\ndef handle_items_without_projects(ap"
  },
  {
    "path": "concordia/migrations/0011_auto_20181022_1532.py",
    "chars": 488,
    "preview": "# Generated by Django 2.0.9 on 2018-10-22 15:32\n\nimport django.db.models.deletion\nfrom django.db import migrations, mode"
  },
  {
    "path": "concordia/migrations/0012_merge_20181022_1554.py",
    "chars": 271,
    "preview": "# Generated by Django 2.0.9 on 2018-10-22 15:54\n\nfrom django.db import migrations\n\n\nclass Migration(migrations.Migration"
  },
  {
    "path": "concordia/migrations/0013_auto_20181031_1305.py",
    "chars": 349,
    "preview": "# Generated by Django 2.0.9 on 2018-10-31 17:05\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "concordia/migrations/0014_auto_20181115_1411.py",
    "chars": 742,
    "preview": "# Generated by Django 2.0.9 on 2018-11-15 19:11\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "concordia/migrations/0015_auto_20181115_1436.py",
    "chars": 767,
    "preview": "# Generated by Django 2.0.9 on 2018-11-15 19:36\n\nfrom django.db import migrations\n\nfrom concordia.models import Transcri"
  },
  {
    "path": "concordia/migrations/0016_auto_20181115_1803.py",
    "chars": 667,
    "preview": "# Generated by Django 2.0.9 on 2018-11-15 23:03\n\nfrom django.db import migrations\n\nfrom concordia.models import Transcri"
  },
  {
    "path": "concordia/migrations/0017_change_transcription_supersedes_related_name.py",
    "chars": 723,
    "preview": "# Generated by Django 2.0.9 on 2018-11-20 17:07\n\nimport django.db.models.deletion\nfrom django.db import migrations, mode"
  },
  {
    "path": "concordia/migrations/0018_auto_20181128_1611.py",
    "chars": 683,
    "preview": "# Generated by Django 2.0.9 on 2018-11-28 21:11\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "concordia/migrations/0018_simplepage.py",
    "chars": 1264,
    "preview": "# Generated by Django 2.0.9 on 2018-11-26 21:58\n\nimport django.core.validators\nfrom django.db import migrations, models\n"
  },
  {
    "path": "concordia/migrations/0019_merge_20181128_1715.py",
    "chars": 263,
    "preview": "# Generated by Django 2.0.9 on 2018-11-28 22:15\n\nfrom django.db import migrations\n\n\nclass Migration(migrations.Migration"
  },
  {
    "path": "concordia/migrations/0020_auto_20181128_1718.py",
    "chars": 358,
    "preview": "# Generated by Django 2.0.9 on 2018-11-28 22:18\n\nfrom django.db import migrations\n\n\nclass Migration(migrations.Migration"
  },
  {
    "path": "concordia/migrations/0021_sitereport.py",
    "chars": 2314,
    "preview": "# Generated by Django 2.0.9 on 2018-12-04 18:26\n\nimport django.db.models.deletion\nfrom django.db import migrations, mode"
  },
  {
    "path": "concordia/migrations/0022_auto_20181211_1310.py",
    "chars": 481,
    "preview": "# Generated by Django 2.0.9 on 2018-12-11 18:10\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "concordia/migrations/0023_auto_20190130_1555.py",
    "chars": 734,
    "preview": "# Generated by Django 2.1.5 on 2019-01-30 20:55\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "concordia/migrations/0024_add_site_report_ordering.py",
    "chars": 326,
    "preview": "# Generated by Django 2.2 on 2019-04-19 15:25\n\nfrom django.db import migrations\n\n\nclass Migration(migrations.Migration):"
  },
  {
    "path": "concordia/migrations/0024_auto_20190211_1420.py",
    "chars": 1723,
    "preview": "# Generated by Django 2.1.7 on 2019-02-11 19:20\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "concordia/migrations/0025_auto_20190329_1705.py",
    "chars": 404,
    "preview": "# Generated by Django 2.1.7 on 2019-03-29 21:05\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "concordia/migrations/0025_unicode_slugs.py",
    "chars": 752,
    "preview": "# Generated by Django 2.2 on 2019-04-19 15:31\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Mig"
  },
  {
    "path": "concordia/migrations/0026_update_published_field_definition.py",
    "chars": 896,
    "preview": "# Generated by Django 2.2 on 2019-04-19 15:41\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Mig"
  },
  {
    "path": "concordia/migrations/0027_merge_20190423_1657.py",
    "chars": 284,
    "preview": "# Generated by Django 2.2 on 2019-04-23 20:57\n\nfrom django.db import migrations\n\n\nclass Migration(migrations.Migration):"
  },
  {
    "path": "concordia/migrations/0028_asset_year.py",
    "chars": 377,
    "preview": "# Generated by Django 2.2 on 2019-04-23 20:57\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Mig"
  },
  {
    "path": "concordia/migrations/0029_assettranscriptionreservation_reservation_token.py",
    "chars": 414,
    "preview": "# Generated by Django 2.2 on 2019-04-23 15:13\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Mig"
  },
  {
    "path": "concordia/migrations/0030_auto_20190503_1559.py",
    "chars": 536,
    "preview": "# Generated by Django 2.2 on 2019-05-03 19:59\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Mig"
  },
  {
    "path": "concordia/migrations/0031_auto_20190509_1142.py",
    "chars": 2263,
    "preview": "# Generated by Django 2.2 on 2019-05-09 15:42\n\nimport django.db.models.deletion\nfrom django.db import migrations, models"
  },
  {
    "path": "concordia/migrations/0032_topic_ordering.py",
    "chars": 482,
    "preview": "# Generated by Django 2.2 on 2019-05-29 18:11\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Mig"
  },
  {
    "path": "concordia/migrations/0033_simple_content_blocks.py",
    "chars": 6528,
    "preview": "# Generated by Django 2.2.2 on 2019-06-21 18:39\n\nfrom django.db import migrations, models\n\n\ndef load_legacy_content_bloc"
  },
  {
    "path": "concordia/migrations/0034_auto_20190627_1438.py",
    "chars": 2906,
    "preview": "# Generated by Django 2.2.2 on 2019-06-27 18:38\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "concordia/migrations/0035_auto_20190627_1455.py",
    "chars": 384,
    "preview": "# Generated by Django 2.2.2 on 2019-06-27 18:55\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "concordia/migrations/0036_auto_20190703_1203.py",
    "chars": 387,
    "preview": "# Generated by Django 2.2.2 on 2019-07-03 16:09\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "concordia/migrations/0037_carouselslide.py",
    "chars": 1784,
    "preview": "# Generated by Django 2.2.3 on 2019-07-31 16:29\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "concordia/migrations/0038_sitereport_topic.py",
    "chars": 558,
    "preview": "# Generated by Django 2.2.3 on 2019-07-31 22:09\n\nimport django.db.models.deletion\nfrom django.db import migrations, mode"
  },
  {
    "path": "concordia/migrations/0039_auto_20200129_1536.py",
    "chars": 1046,
    "preview": "# Generated by Django 2.2.7 on 2020-01-29 20:36\n\nimport django.db.models.deletion\nfrom django.db import migrations, mode"
  },
  {
    "path": "concordia/migrations/0040_auto_20200130_1756.py",
    "chars": 729,
    "preview": "# Generated by Django 2.2.7 on 2020-01-30 22:56\n\nimport django.db.models.deletion\nfrom django.db import migrations, mode"
  },
  {
    "path": "concordia/migrations/0041_auto_20200203_1351.py",
    "chars": 385,
    "preview": "# Generated by Django 2.2.7 on 2020-02-03 18:51\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "concordia/migrations/0042_auto_20200316_1623.py",
    "chars": 572,
    "preview": "# Generated by Django 2.2.10 on 2020-03-16 20:23\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0043_auto_20200323_1729.py",
    "chars": 1535,
    "preview": "# Generated by Django 2.2.11 on 2020-03-23 21:29\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0044_auto_20200323_1827.py",
    "chars": 889,
    "preview": "# Generated by Django 2.2.11 on 2020-03-23 22:27\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0045_auto_20200323_1832.py",
    "chars": 471,
    "preview": "# Generated by Django 2.2.11 on 2020-03-23 22:32\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0046_auto_20200323_1907.py",
    "chars": 782,
    "preview": "# Generated by Django 2.2.11 on 2020-03-23 23:07\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0047_auto_20200324_1103.py",
    "chars": 442,
    "preview": "# Generated by Django 2.2.11 on 2020-03-24 15:03\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0048_auto_20200324_1820.py",
    "chars": 1357,
    "preview": "# Generated by Django 2.2.11 on 2020-03-24 22:20\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0049_auto_20200324_2004.py",
    "chars": 422,
    "preview": "# Generated by Django 2.2.11 on 2020-03-25 00:04\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0050_auto_20210920_1544.py",
    "chars": 843,
    "preview": "# Generated by Django 2.2.20 on 2021-09-20 19:44\n\nimport django.core.validators\nfrom django.db import migrations, models"
  },
  {
    "path": "concordia/migrations/0051_asset_storage_image.py",
    "chars": 572,
    "preview": "# Generated by Django 2.2.24 on 2022-01-11 18:14\n\nfrom django.db import migrations, models\n\nimport concordia.models\n\n\ncl"
  },
  {
    "path": "concordia/migrations/0052_auto_20220531_1331.py",
    "chars": 1335,
    "preview": "# Generated by Django 3.2.13 on 2022-05-31 17:31\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0053_banner.py",
    "chars": 1046,
    "preview": "# Generated by Django 3.2.14 on 2022-08-09 17:28\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0054_banner_active.py",
    "chars": 389,
    "preview": "# Generated by Django 3.2.14 on 2022-09-16 17:10\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0055_campaign_status.py",
    "chars": 471,
    "preview": "# Generated by Django 3.2.15 on 2022-09-19 19:16\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0056_auto_20220922_1508.py",
    "chars": 567,
    "preview": "# Generated by Django 3.2.15 on 2022-09-22 19:08\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0057_resource_resource_type.py",
    "chars": 509,
    "preview": "# Generated by Django 3.2.15 on 2022-09-26 17:20\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0058_banner_slug.py",
    "chars": 507,
    "preview": "# Generated by Django 3.2.14 on 2022-10-18 17:28\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0059_resourcefile.py",
    "chars": 841,
    "preview": "# Generated by Django 3.2.15 on 2022-12-17 20:47\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0060_alter_resourcefile_resource.py",
    "chars": 413,
    "preview": "# Generated by Django 3.2.15 on 2022-12-17 21:53\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0061_auto_20230201_1453.py",
    "chars": 679,
    "preview": "# Generated by Django 3.2.15 on 2023-02-01 19:53\n\nfrom django.db import migrations, models\n\nimport concordia.models\n\n\ncl"
  },
  {
    "path": "concordia/migrations/0061_sitereport_registered_contributors.py",
    "chars": 427,
    "preview": "# Generated by Django 3.2.15 on 2022-12-15 01:18\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0062_resourcefile_updated_on.py",
    "chars": 400,
    "preview": "# Generated by Django 3.2.15 on 2023-02-07 20:45\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0062_userretiredcampaign.py",
    "chars": 1962,
    "preview": "# Generated by Django 3.2.14 on 2023-01-20 18:42\n\nimport django.db.models.deletion\nfrom django.conf import settings\nfrom"
  },
  {
    "path": "concordia/migrations/0063_banner_alert_status.py",
    "chars": 679,
    "preview": "# Generated by Django 3.2.17 on 2023-02-16 17:33\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0064_alter_banner_alert_status.py",
    "chars": 784,
    "preview": "# Generated by Django 3.2.17 on 2023-02-23 17:37\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0065_alter_userretiredcampaign_unique_together.py",
    "chars": 471,
    "preview": "# Generated by Django 3.2.14 on 2023-02-13 20:51\n\nfrom django.conf import settings\nfrom django.db import migrations\n\n\ncl"
  },
  {
    "path": "concordia/migrations/0066_auto_20230217_1302.py",
    "chars": 1531,
    "preview": "# Generated by Django 3.2.17 on 2023-02-17 13:02\n\nimport django.db.models.deletion\nfrom django.conf import settings\nfrom"
  },
  {
    "path": "concordia/migrations/0066_campaignretirementprogress.py",
    "chars": 1360,
    "preview": "# Generated by Django 3.2.16 on 2023-02-22 20:26\n\nimport django.db.models.deletion\nfrom django.db import migrations, mod"
  },
  {
    "path": "concordia/migrations/0067_alter_campaignretirementprogress_campaign.py",
    "chars": 540,
    "preview": "# Generated by Django 3.2.16 on 2023-02-22 20:56\n\nimport django.db.models.deletion\nfrom django.db import migrations, mod"
  },
  {
    "path": "concordia/migrations/0068_campaignretirementprogress_complete.py",
    "chars": 434,
    "preview": "# Generated by Django 3.2.16 on 2023-02-23 20:35\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0069_merge_20230224_1446.py",
    "chars": 289,
    "preview": "# Generated by Django 3.2.16 on 2023-02-24 19:46\n\nfrom django.db import migrations\n\n\nclass Migration(migrations.Migratio"
  },
  {
    "path": "concordia/migrations/0070_alter_campaign_options.py",
    "chars": 389,
    "preview": "# Generated by Django 3.2.16 on 2023-02-27 16:29\n\nfrom django.db import migrations\n\n\nclass Migration(migrations.Migratio"
  },
  {
    "path": "concordia/migrations/0071_auto_20230306_1456.py",
    "chars": 904,
    "preview": "# Generated by Django 3.2.16 on 2023-03-06 19:56\n\nimport django.utils.timezone\nfrom django.db import migrations, models\n"
  },
  {
    "path": "concordia/migrations/0072_merge_20230313_1047.py",
    "chars": 279,
    "preview": "# Generated by Django 3.2.18 on 2023-03-13 14:47\n\nfrom django.db import migrations\n\n\nclass Migration(migrations.Migratio"
  },
  {
    "path": "concordia/migrations/0073_auto_20230314_1327.py",
    "chars": 2740,
    "preview": "# Generated by Django 3.2.18 on 2023-03-14 13:27\n\nimport django.db.models.deletion\nfrom django.conf import settings\nfrom"
  },
  {
    "path": "concordia/migrations/0074_auto_20230314_1341.py",
    "chars": 744,
    "preview": "# Generated by Django 3.2.18 on 2023-03-14 13:41\n\nfrom django.db import migrations\n\n\ndef forwards_func(apps, schema_edit"
  },
  {
    "path": "concordia/migrations/0075_auto_20230327_1333.py",
    "chars": 540,
    "preview": "# Generated by Django 3.2.18 on 2023-03-27 17:33\n\nfrom django.db import migrations\n\n\nclass Migration(migrations.Migratio"
  },
  {
    "path": "concordia/migrations/0076_sitereport_report_name.py",
    "chars": 419,
    "preview": "# Generated by Django 3.2.18 on 2023-04-27 17:17\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0077_alter_sitereport_report_name.py",
    "chars": 1568,
    "preview": "# Generated by Django 3.2.18 on 2023-05-04 15:33\n\nfrom django.db import migrations, models\n\n\ndef update_report_names(app"
  },
  {
    "path": "concordia/migrations/0078_alter_sitereport_report_name.py",
    "chars": 1634,
    "preview": "# Generated by Django 3.2.18 on 2023-05-08 15:13\n\nfrom django.db import migrations, models\n\n\ndef update_report_names(app"
  },
  {
    "path": "concordia/migrations/0079_auto_20230601_1234.py",
    "chars": 1086,
    "preview": "# Generated by Django 3.2.18 on 2023-06-01 16:34\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0080_auto_20230602_0920.py",
    "chars": 801,
    "preview": "# Generated by Django 3.2.19 on 2023-06-02 13:20\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0081_sitereport_review_actions.py",
    "chars": 409,
    "preview": "# Generated by Django 3.2.19 on 2023-06-28 16:53\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0082_delete_userretiredcampaign.py",
    "chars": 317,
    "preview": "# Generated by Django 3.2.19 on 2023-07-10 13:06\n\nfrom django.db import migrations\n\n\nclass Migration(migrations.Migratio"
  },
  {
    "path": "concordia/migrations/0083_sitereport_daily_active_users.py",
    "chars": 421,
    "preview": "# Generated by Django 3.2.19 on 2023-07-10 14:47\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0084_rename_review_actions_sitereport_daily_review_actions.py",
    "chars": 402,
    "preview": "# Generated by Django 3.2.19 on 2023-07-21 14:36\n\nfrom django.db import migrations\n\n\nclass Migration(migrations.Migratio"
  },
  {
    "path": "concordia/migrations/0085_auto_20231016_1432.py",
    "chars": 537,
    "preview": "# Generated by Django 3.2.22 on 2023-10-16 18:32\n\nfrom django.db import migrations, models\n\nimport concordia.models\n\n\ncl"
  },
  {
    "path": "concordia/migrations/0086_auto_20231215_1311.py",
    "chars": 1106,
    "preview": "# Generated by Django 3.2.23 on 2023-12-15 18:11\n\nimport django.contrib.auth.models\nfrom django.db import migrations, mo"
  },
  {
    "path": "concordia/migrations/0087_auto_20240213_0756.py",
    "chars": 4493,
    "preview": "# Generated by Django 3.2.23 on 2024-02-13 12:56\n\nimport django.db.models.deletion\nfrom django.db import migrations, mod"
  },
  {
    "path": "concordia/migrations/0088_alter_simplepage_body.py",
    "chars": 399,
    "preview": "# Generated by Django 3.2.24 on 2024-02-21 18:37\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0089_campaign_image_alt_text.py",
    "chars": 408,
    "preview": "# Generated by Django 3.2.24 on 2024-02-26 14:13\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0090_auto_20240408_1334.py",
    "chars": 1207,
    "preview": "# Generated by Django 3.2.25 on 2024-04-09 15:25\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0091_guide_simple_page.py",
    "chars": 578,
    "preview": "# Generated by Django 4.2.13 on 2024-05-09 19:21\n\nimport django.db.models.deletion\nfrom django.db import migrations, mod"
  },
  {
    "path": "concordia/migrations/0092_auto_20240509_1522.py",
    "chars": 751,
    "preview": "# Generated by Django 4.2.13 on 2024-05-09 19:22\n\nfrom django.db import migrations\n\n\ndef set_simplepages(apps, schema_ed"
  },
  {
    "path": "concordia/migrations/0093_asset_campaign.py",
    "chars": 551,
    "preview": "# Generated by Django 4.2.13 on 2024-06-17 17:13\n\nimport django.db.models.deletion\nfrom django.db import migrations, mod"
  },
  {
    "path": "concordia/migrations/0094_alter_asset_campaign.py",
    "chars": 1656,
    "preview": "# Generated by Django 4.2.13 on 2024-06-17 17:13\n\nimport django.db.models.deletion\nfrom django.db import migrations, mod"
  },
  {
    "path": "concordia/migrations/0095_transcription_rolled_back_and_more.py",
    "chars": 818,
    "preview": "# Generated by Django 4.2.13 on 2024-06-26 23:26\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0096_transcription_source.py",
    "chars": 736,
    "preview": "# Generated by Django 4.2.13 on 2024-06-26 23:41\n\nimport django.db.models.deletion\nfrom django.db import migrations, mod"
  },
  {
    "path": "concordia/migrations/0097_alter_sitereport_options_userprofile_review_count_and_more.py",
    "chars": 1317,
    "preview": "# Generated by Django 4.2.13 on 2024-07-29 17:30\n\nimport django.db.models.deletion\nfrom django.conf import settings\nfrom"
  },
  {
    "path": "concordia/migrations/0098_userprofile_create_and_population.py",
    "chars": 1349,
    "preview": "# Generated by Django 4.2.13 on 2024-07-29 17:40\n\nfrom django.conf import settings\nfrom django.db import migrations\n\n\nde"
  },
  {
    "path": "concordia/migrations/0099_alter_campaign_display_on_homepage_and_more.py",
    "chars": 964,
    "preview": "# Generated by Django 4.2.16 on 2024-11-01 17:49\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0100_researchcenter.py",
    "chars": 728,
    "preview": "# Generated by Django 4.2.16 on 2024-11-19 17:15\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0101_auto_20241119_1215.py",
    "chars": 1168,
    "preview": "# Generated by Django 4.2.16 on 2024-11-19 17:15\n\nfrom django.db import migrations\n\nTITLES = (\n    \"American Folklife Ce"
  },
  {
    "path": "concordia/migrations/0102_campaign_research_centers.py",
    "chars": 433,
    "preview": "# Generated by Django 4.2.16 on 2024-11-20 12:06\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0103_alter_item_title.py",
    "chars": 394,
    "preview": "# Generated by Django 4.2.16 on 2024-12-16 18:03\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "concordia/migrations/0104_nexttranscribabletopicasset_and_more.py",
    "chars": 9154,
    "preview": "# Generated by Django 4.2.16 on 2025-04-04 18:55\n\nimport uuid\n\nimport django.contrib.postgres.fields\nimport django.db.mo"
  }
]

// ... and 473 more files (download for full content)

About this extraction

This page contains the full source code of the LibraryOfCongress/concordia GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 673 files (3.0 MB), approximately 832.6k tokens, and a symbol index with 2770 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!